OSDN Git Service

48bbc0cc3725f66ba56a81ca2b28eea73654f419
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / CalendarView.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.calendar;
18
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
21
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.Path;
34 import android.graphics.PorterDuff;
35 import android.graphics.Rect;
36 import android.graphics.RectF;
37 import android.graphics.Typeface;
38 import android.graphics.Paint.Style;
39 import android.graphics.Path.Direction;
40 import android.net.Uri;
41 import android.os.Handler;
42 import android.provider.Calendar.Attendees;
43 import android.provider.Calendar.Calendars;
44 import android.provider.Calendar.Events;
45 import android.text.TextUtils;
46 import android.text.format.DateFormat;
47 import android.text.format.DateUtils;
48 import android.text.format.Time;
49 import android.util.Log;
50 import android.view.ContextMenu;
51 import android.view.Gravity;
52 import android.view.KeyEvent;
53 import android.view.LayoutInflater;
54 import android.view.MenuItem;
55 import android.view.MotionEvent;
56 import android.view.View;
57 import android.view.ViewConfiguration;
58 import android.view.ViewGroup;
59 import android.view.WindowManager;
60 import android.view.ContextMenu.ContextMenuInfo;
61 import android.widget.ImageView;
62 import android.widget.PopupWindow;
63 import android.widget.TextView;
64
65 import java.util.ArrayList;
66 import java.util.Calendar;
67 import java.util.regex.Matcher;
68 import java.util.regex.Pattern;
69
70 /**
71  * This is the base class for a set of classes that implement views (day view
72  * and week view to start with) that share some common code.
73   */
74 public class CalendarView extends View
75         implements View.OnCreateContextMenuListener, View.OnClickListener {
76
77     private static float mScale = 0; // Used for supporting different screen densities
78     private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
79
80     private boolean mOnFlingCalled;
81     /**
82      * ID of the last event which was displayed with the toast popup.
83      *
84      * This is used to prevent popping up multiple quick views for the same event, especially
85      * during calendar syncs. This becomes valid when an event is selected, either by default
86      * on starting calendar or by scrolling to an event. It becomes invalid when the user
87      * explicitly scrolls to an empty time slot, changes views, or deletes the event.
88      */
89     private long mLastPopupEventID;
90
91     protected CalendarApplication mCalendarApp;
92     protected CalendarActivity mParentActivity;
93
94     private static final String[] CALENDARS_PROJECTION = new String[] {
95         Calendars._ID,          // 0
96         Calendars.ACCESS_LEVEL, // 1
97         Calendars.OWNER_ACCOUNT, // 2
98     };
99     private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
100     private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
101     private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
102
103     private static final String[] ATTENDEES_PROJECTION = new String[] {
104         Attendees._ID,                      // 0
105         Attendees.ATTENDEE_RELATIONSHIP,    // 1
106     };
107     private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
108     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
109
110     private static float SMALL_ROUND_RADIUS = 3.0F;
111
112     private static final int FROM_NONE = 0;
113     private static final int FROM_ABOVE = 1;
114     private static final int FROM_BELOW = 2;
115     private static final int FROM_LEFT = 4;
116     private static final int FROM_RIGHT = 8;
117
118     private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
119
120     private ContinueScroll mContinueScroll = new ContinueScroll();
121
122     static private class DayHeader{
123         int cell;
124         String dateString;
125     }
126
127     private DayHeader[] dayHeaders = new DayHeader[32];
128
129     // Make this visible within the package for more informative debugging
130     Time mBaseDate;
131     private Time mCurrentTime;
132     //Update the current time line every five minutes if the window is left open that long
133     private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
134     private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
135     private int mTodayJulianDay;
136
137     private Typeface mBold = Typeface.DEFAULT_BOLD;
138     private int mFirstJulianDay;
139     private int mLastJulianDay;
140
141     private int mMonthLength;
142     private int mFirstDate;
143     private int[] mEarliestStartHour;    // indexed by the week day offset
144     private boolean[] mHasAllDayEvent;   // indexed by the week day offset
145
146     private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
147
148     /**
149      * This variable helps to avoid unnecessarily reloading events by keeping
150      * track of the start millis parameter used for the most recent loading
151      * of events.  If the next reload matches this, then the events are not
152      * reloaded.  To force a reload, set this to zero (this is set to zero
153      * in the method clearCachedEvents()).
154      */
155     private long mLastReloadMillis;
156
157     private ArrayList<Event> mEvents = new ArrayList<Event>();
158     private int mSelectionDay;        // Julian day
159     private int mSelectionHour;
160
161     /* package private so that CalendarActivity can read it when creating new
162      * events
163      */
164     boolean mSelectionAllDay;
165
166     private int mCellWidth;
167
168     // Pre-allocate these objects and re-use them
169     private Rect mRect = new Rect();
170     private RectF mRectF = new RectF();
171     private Rect mSrcRect = new Rect();
172     private Rect mDestRect = new Rect();
173     private Paint mPaint = new Paint();
174     private Paint mPaintBorder = new Paint();
175     private Paint mEventTextPaint = new Paint();
176     private Paint mSelectionPaint = new Paint();
177     private Path mPath = new Path();
178
179     protected boolean mDrawTextInEventRect;
180     private int mStartDay;
181
182     private PopupWindow mPopup;
183     private View mPopupView;
184
185     // The number of milliseconds to show the popup window
186     private static final int POPUP_DISMISS_DELAY = 3000;
187     private DismissPopup mDismissPopup = new DismissPopup();
188
189     // For drawing to an off-screen Canvas
190     private Bitmap mBitmap;
191     private Canvas mCanvas;
192     private boolean mRedrawScreen = true;
193     private boolean mRemeasure = true;
194
195     private final EventLoader mEventLoader;
196     protected final EventGeometry mEventGeometry;
197
198     private static final int DAY_GAP = 1;
199     private static final int HOUR_GAP = 1;
200     private static int SINGLE_ALLDAY_HEIGHT = 20;
201     private static int MAX_ALLDAY_HEIGHT = 72;
202     private static int ALLDAY_TOP_MARGIN = 3;
203     private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
204
205     /* The extra space to leave above the text in all-day events */
206     private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
207
208     /* The extra space to leave above the text in normal events */
209     private static final int NORMAL_TEXT_TOP_MARGIN = 2;
210
211     private static final int HOURS_LEFT_MARGIN = 2;
212     private static final int HOURS_RIGHT_MARGIN = 4;
213     private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
214
215     private static int CURRENT_TIME_LINE_HEIGHT = 2;
216     private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
217     private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6;
218     private static int CURRENT_TIME_MARKER_HEIGHT = 6;
219     private static int CURRENT_TIME_MARKER_WIDTH = 8;
220     private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1;
221
222     /* package */ static final int MINUTES_PER_HOUR = 60;
223     /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
224     /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
225     /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
226     /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
227
228     private static int NORMAL_FONT_SIZE = 12;
229     private static int EVENT_TEXT_FONT_SIZE = 12;
230     private static int HOURS_FONT_SIZE = 12;
231     private static int AMPM_FONT_SIZE = 9;
232     private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
233     private static final int MAX_EVENT_TEXT_LEN = 500;
234     private static float MIN_EVENT_HEIGHT = 15.0F;  // in pixels
235
236     private static int mSelectionColor;
237     private static int mPressedColor;
238     private static int mSelectedEventTextColor;
239     private static int mEventTextColor;
240     private static int mWeek_saturdayColor;
241     private static int mWeek_sundayColor;
242     private static int mCalendarDateBannerTextColor;
243     private static int mCalendarAllDayBackground;
244     private static int mCalendarAmPmLabel;
245     private static int mCalendarDateBannerBackground;
246     private static int mCalendarDateSelected;
247     private static int mCalendarGridAreaBackground;
248     private static int mCalendarGridAreaSelected;
249     private static int mCalendarGridLineHorizontalColor;
250     private static int mCalendarGridLineVerticalColor;
251     private static int mCalendarHourBackground;
252     private static int mCalendarHourLabel;
253     private static int mCalendarHourSelected;
254     private static int mCurrentTimeMarkerColor;
255     private static int mCurrentTimeMarkerBorderColor;
256
257     private int mViewStartX;
258     private int mViewStartY;
259     private int mMaxViewStartY;
260     private int mBitmapHeight;
261     private int mViewHeight;
262     private int mViewWidth;
263     private int mGridAreaHeight;
264     private int mCellHeight;
265     private int mScrollStartY;
266     private int mPreviousDirection;
267     private int mPreviousDistanceX;
268
269     private int mHoursTextHeight;
270     private int mEventTextAscent;
271     private int mEventTextHeight;
272     private int mAllDayHeight;
273     private int mBannerPlusMargin;
274     private int mMaxAllDayEvents;
275
276     protected int mNumDays = 7;
277     private int mNumHours = 10;
278     private int mHoursWidth;
279     private int mDateStrWidth;
280     private int mFirstCell;
281     private int mFirstHour = -1;
282     private int mFirstHourOffset;
283     private String[] mHourStrs;
284     private String[] mDayStrs;
285     private String[] mDayStrs2Letter;
286     private boolean mIs24HourFormat;
287
288     private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
289     private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
290     private boolean mComputeSelectedEvents;
291     private Event mSelectedEvent;
292     private Event mPrevSelectedEvent;
293     private Rect mPrevBox = new Rect();
294     protected final Resources mResources;
295     private String mAmString;
296     private String mPmString;
297     private DeleteEventHelper mDeleteEventHelper;
298
299     private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
300
301     /**
302      * The initial state of the touch mode when we enter this view.
303      */
304     private static final int TOUCH_MODE_INITIAL_STATE = 0;
305
306     /**
307      * Indicates we just received the touch event and we are waiting to see if
308      * it is a tap or a scroll gesture.
309      */
310     private static final int TOUCH_MODE_DOWN = 1;
311
312     /**
313      * Indicates the touch gesture is a vertical scroll
314      */
315     private static final int TOUCH_MODE_VSCROLL = 0x20;
316
317     /**
318      * Indicates the touch gesture is a horizontal scroll
319      */
320     private static final int TOUCH_MODE_HSCROLL = 0x40;
321
322     private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
323
324     /**
325      * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
326      */
327     private static final int SELECTION_HIDDEN = 0;
328     private static final int SELECTION_PRESSED = 1;
329     private static final int SELECTION_SELECTED = 2;
330     private static final int SELECTION_LONGPRESS = 3;
331
332     private int mSelectionMode = SELECTION_HIDDEN;
333
334     private boolean mScrolling = false;
335
336     private String mDateRange;
337     private TextView mTitleTextView;
338
339     public CalendarView(CalendarActivity activity) {
340         super(activity);
341         if (mScale == 0) {
342             mScale = getContext().getResources().getDisplayMetrics().density;
343             if (mScale != 1) {
344                 SINGLE_ALLDAY_HEIGHT *= mScale;
345                 MAX_ALLDAY_HEIGHT *= mScale;
346                 ALLDAY_TOP_MARGIN *= mScale;
347                 MAX_ALLDAY_EVENT_HEIGHT *= mScale;
348
349                 NORMAL_FONT_SIZE *= mScale;
350                 EVENT_TEXT_FONT_SIZE *= mScale;
351                 HOURS_FONT_SIZE *= mScale;
352                 AMPM_FONT_SIZE *= mScale;
353                 MIN_CELL_WIDTH_FOR_TEXT *= mScale;
354                 MIN_EVENT_HEIGHT *= mScale;
355
356                 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
357
358                 CURRENT_TIME_MARKER_HEIGHT *= mScale;
359                 CURRENT_TIME_MARKER_WIDTH *= mScale;
360                 CURRENT_TIME_LINE_HEIGHT *= mScale;
361                 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
362                 CURRENT_TIME_MARKER_INNER_WIDTH *= mScale;
363                 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;
364
365                 SMALL_ROUND_RADIUS *= mScale;
366             }
367         }
368
369         mResources = activity.getResources();
370         mEventLoader = activity.mEventLoader;
371         mEventGeometry = new EventGeometry();
372         mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
373         mEventGeometry.setHourGap(HOUR_GAP);
374         mParentActivity = activity;
375         mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
376         mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
377         mLastPopupEventID = INVALID_EVENT_ID;
378
379         init(activity);
380     }
381
382     private void init(Context context) {
383         setFocusable(true);
384
385         // Allow focus in touch mode so that we can do keyboard shortcuts
386         // even after we've entered touch mode.
387         setFocusableInTouchMode(true);
388         setClickable(true);
389         setOnCreateContextMenuListener(this);
390
391         mStartDay = Utils.getFirstDayOfWeek();
392
393         mCurrentTime = new Time();
394         long currentTime = System.currentTimeMillis();
395         mCurrentTime.set(currentTime);
396         //The % makes it go off at the next increment of 5 minutes.
397         postDelayed(mUpdateCurrentTime,
398                 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
399         mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
400
401         mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
402         mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
403         mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
404         mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
405         mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
406         mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
407         mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
408         mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
409         mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
410         mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
411         mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
412         mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
413         mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
414         mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
415         mSelectionColor = mResources.getColor(R.color.selection);
416         mPressedColor = mResources.getColor(R.color.pressed);
417         mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
418         mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
419         mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker);
420         mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border);
421         mEventTextPaint.setColor(mEventTextColor);
422         mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
423         mEventTextPaint.setTextAlign(Paint.Align.LEFT);
424         mEventTextPaint.setAntiAlias(true);
425
426         int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
427         Paint p = mSelectionPaint;
428         p.setColor(gridLineColor);
429         p.setStyle(Style.STROKE);
430         p.setStrokeWidth(2.0f);
431         p.setAntiAlias(false);
432
433         p = mPaint;
434         p.setAntiAlias(true);
435
436         mPaintBorder.setColor(0xffc8c8c8);
437         mPaintBorder.setStyle(Style.STROKE);
438         mPaintBorder.setAntiAlias(true);
439         mPaintBorder.setStrokeWidth(2.0f);
440
441         // Allocate space for 2 weeks worth of weekday names so that we can
442         // easily start the week display at any week day.
443         mDayStrs = new String[14];
444
445         // Also create an array of 2-letter abbreviations.
446         mDayStrs2Letter = new String[14];
447
448         for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
449             int index = i - Calendar.SUNDAY;
450             // e.g. Tue for Tuesday
451             mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
452             mDayStrs[index + 7] = mDayStrs[index];
453             // e.g. Tu for Tuesday
454             mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
455
456             // If we don't have 2-letter day strings, fall back to 1-letter.
457             if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
458                 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
459             }
460
461             mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
462         }
463
464         // Figure out how much space we need for the 3-letter abbrev names
465         // in the worst case.
466         p.setTextSize(NORMAL_FONT_SIZE);
467         p.setTypeface(mBold);
468         String[] dateStrs = {" 28", " 30"};
469         mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
470         mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
471
472         p.setTextSize(HOURS_FONT_SIZE);
473         p.setTypeface(null);
474         mIs24HourFormat = DateFormat.is24HourFormat(context);
475         mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
476         mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
477
478         mAmString = DateUtils.getAMPMString(Calendar.AM);
479         mPmString = DateUtils.getAMPMString(Calendar.PM);
480         String[] ampm = {mAmString, mPmString};
481         p.setTextSize(AMPM_FONT_SIZE);
482         mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
483         mHoursWidth += HOURS_MARGIN;
484
485         LayoutInflater inflater;
486         inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
487         mPopupView = inflater.inflate(R.layout.bubble_event, null);
488         mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
489                 ViewGroup.LayoutParams.MATCH_PARENT,
490                 ViewGroup.LayoutParams.WRAP_CONTENT));
491         mPopup = new PopupWindow(context);
492         mPopup.setContentView(mPopupView);
493         Resources.Theme dialogTheme = getResources().newTheme();
494         dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
495         TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
496             android.R.attr.windowBackground });
497         mPopup.setBackgroundDrawable(ta.getDrawable(0));
498         ta.recycle();
499
500         // Enable touching the popup window
501         mPopupView.setOnClickListener(this);
502
503         mBaseDate = new Time();
504         long millis = System.currentTimeMillis();
505         mBaseDate.set(millis);
506
507         mEarliestStartHour = new int[mNumDays];
508         mHasAllDayEvent = new boolean[mNumDays];
509
510         mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
511         mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
512     }
513
514     /**
515      * This is called when the popup window is pressed.
516      */
517     public void onClick(View v) {
518         if (v == mPopupView) {
519             // Pretend it was a trackball click because that will always
520             // jump to the "View event" screen.
521             switchViews(true /* trackball */);
522         }
523     }
524
525     /**
526      * Returns the start of the selected time in milliseconds since the epoch.
527      *
528      * @return selected time in UTC milliseconds since the epoch.
529      */
530     long getSelectedTimeInMillis() {
531         Time time = new Time(mBaseDate);
532         time.setJulianDay(mSelectionDay);
533         time.hour = mSelectionHour;
534
535         // We ignore the "isDst" field because we want normalize() to figure
536         // out the correct DST value and not adjust the selected time based
537         // on the current setting of DST.
538         return time.normalize(true /* ignore isDst */);
539     }
540
541     Time getSelectedTime() {
542         Time time = new Time(mBaseDate);
543         time.setJulianDay(mSelectionDay);
544         time.hour = mSelectionHour;
545
546         // We ignore the "isDst" field because we want normalize() to figure
547         // out the correct DST value and not adjust the selected time based
548         // on the current setting of DST.
549         time.normalize(true /* ignore isDst */);
550         return time;
551     }
552
553     /**
554      * Returns the start of the selected time in minutes since midnight,
555      * local time.  The derived class must ensure that this is consistent
556      * with the return value from getSelectedTimeInMillis().
557      */
558     int getSelectedMinutesSinceMidnight() {
559         return mSelectionHour * MINUTES_PER_HOUR;
560     }
561
562     public void setSelectedDay(Time time) {
563         mBaseDate.set(time);
564         mSelectionHour = mBaseDate.hour;
565         mSelectedEvent = null;
566         mPrevSelectedEvent = null;
567         long millis = mBaseDate.toMillis(false /* use isDst */);
568         mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
569         mSelectedEvents.clear();
570         mComputeSelectedEvents = true;
571
572         // Force a recalculation of the first visible hour
573         mFirstHour = -1;
574         recalc();
575         mTitleTextView.setText(mDateRange);
576
577         // Force a redraw of the selection box.
578         mSelectionMode = SELECTION_SELECTED;
579         mRedrawScreen = true;
580         mRemeasure = true;
581         invalidate();
582     }
583
584     public Time getSelectedDay() {
585         Time time = new Time(mBaseDate);
586         time.setJulianDay(mSelectionDay);
587         time.hour = mSelectionHour;
588
589         // We ignore the "isDst" field because we want normalize() to figure
590         // out the correct DST value and not adjust the selected time based
591         // on the current setting of DST.
592         time.normalize(true /* ignore isDst */);
593         return time;
594     }
595
596     private void recalc() {
597         // Set the base date to the beginning of the week if we are displaying
598         // 7 days at a time.
599         if (mNumDays == 7) {
600             int dayOfWeek = mBaseDate.weekDay;
601             int diff = dayOfWeek - mStartDay;
602             if (diff != 0) {
603                 if (diff < 0) {
604                     diff += 7;
605                 }
606                 mBaseDate.monthDay -= diff;
607                 mBaseDate.normalize(true /* ignore isDst */);
608             }
609         }
610
611         final long start = mBaseDate.toMillis(false /* use isDst */);
612         long end = start;
613         mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
614         mLastJulianDay = mFirstJulianDay + mNumDays - 1;
615
616         mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
617         mFirstDate = mBaseDate.monthDay;
618
619         int flags = DateUtils.FORMAT_SHOW_YEAR;
620         if (DateFormat.is24HourFormat(mParentActivity)) {
621             flags |= DateUtils.FORMAT_24HOUR;
622         }
623         if (mNumDays > 1) {
624             mBaseDate.monthDay += mNumDays - 1;
625             end = mBaseDate.toMillis(true /* ignore isDst */);
626             mBaseDate.monthDay -= mNumDays - 1;
627             flags |= DateUtils.FORMAT_NO_MONTH_DAY;
628         } else {
629             flags |= DateUtils.FORMAT_SHOW_WEEKDAY
630                     | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
631         }
632
633         mDateRange = DateUtils.formatDateRange(mParentActivity, start, end, flags);
634         // Do not set the title here because this is called when executing
635         // initNextView() to prepare the Day view when sliding the finger
636         // horizontally but we don't always want to change the title.  And
637         // if we change the title here and then change it back in the caller
638         // then we get an annoying flicker.
639     }
640
641     void setDetailedView(String detailedView) {
642         mDetailedView = detailedView;
643     }
644
645     @Override
646     protected void onSizeChanged(int width, int height, int oldw, int oldh) {
647         mViewWidth = width;
648         mViewHeight = height;
649         int gridAreaWidth = width - mHoursWidth;
650         mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
651
652         Paint p = new Paint();
653         p.setTextSize(NORMAL_FONT_SIZE);
654         int bannerTextHeight = (int) Math.abs(p.ascent());
655
656         p.setTextSize(HOURS_FONT_SIZE);
657         mHoursTextHeight = (int) Math.abs(p.ascent());
658
659         p.setTextSize(EVENT_TEXT_FONT_SIZE);
660         float ascent = -p.ascent();
661         mEventTextAscent = (int) Math.ceil(ascent);
662         float totalHeight = ascent + p.descent();
663         mEventTextHeight = (int) Math.ceil(totalHeight);
664
665         if (mNumDays > 1) {
666             mBannerPlusMargin = bannerTextHeight + 14;
667         } else {
668             mBannerPlusMargin = 0;
669         }
670
671         remeasure(width, height);
672     }
673
674     // Measures the space needed for various parts of the view after
675     // loading new events.  This can change if there are all-day events.
676     private void remeasure(int width, int height) {
677
678         // First, clear the array of earliest start times, and the array
679         // indicating presence of an all-day event.
680         for (int day = 0; day < mNumDays; day++) {
681             mEarliestStartHour[day] = 25;  // some big number
682             mHasAllDayEvent[day] = false;
683         }
684
685         // Compute the space needed for the all-day events, if any.
686         // Make a pass over all the events, and keep track of the maximum
687         // number of all-day events in any one day.  Also, keep track of
688         // the earliest event in each day.
689         int maxAllDayEvents = 0;
690         ArrayList<Event> events = mEvents;
691         int len = events.size();
692         for (int ii = 0; ii < len; ii++) {
693             Event event = events.get(ii);
694             if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
695                 continue;
696             if (event.allDay) {
697                 int max = event.getColumn() + 1;
698                 if (maxAllDayEvents < max) {
699                     maxAllDayEvents = max;
700                 }
701                 int daynum = event.startDay - mFirstJulianDay;
702                 int durationDays = event.endDay - event.startDay + 1;
703                 if (daynum < 0) {
704                     durationDays += daynum;
705                     daynum = 0;
706                 }
707                 if (daynum + durationDays > mNumDays) {
708                     durationDays = mNumDays - daynum;
709                 }
710                 for (int day = daynum; durationDays > 0; day++, durationDays--) {
711                     mHasAllDayEvent[day] = true;
712                 }
713             } else {
714                 int daynum = event.startDay - mFirstJulianDay;
715                 int hour = event.startTime / 60;
716                 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
717                     mEarliestStartHour[daynum] = hour;
718                 }
719
720                 // Also check the end hour in case the event spans more than
721                 // one day.
722                 daynum = event.endDay - mFirstJulianDay;
723                 hour = event.endTime / 60;
724                 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
725                     mEarliestStartHour[daynum] = hour;
726                 }
727             }
728         }
729         mMaxAllDayEvents = maxAllDayEvents;
730
731         mFirstCell = mBannerPlusMargin;
732         int allDayHeight = 0;
733         if (maxAllDayEvents > 0) {
734             // If there is at most one all-day event per day, then use less
735             // space (but more than the space for a single event).
736             if (maxAllDayEvents == 1) {
737                 allDayHeight = SINGLE_ALLDAY_HEIGHT;
738             } else {
739                 // Allow the all-day area to grow in height depending on the
740                 // number of all-day events we need to show, up to a limit.
741                 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
742                 if (allDayHeight > MAX_ALLDAY_HEIGHT) {
743                     allDayHeight = MAX_ALLDAY_HEIGHT;
744                 }
745             }
746             mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
747         } else {
748             mSelectionAllDay = false;
749         }
750         mAllDayHeight = allDayHeight;
751
752         mGridAreaHeight = height - mFirstCell;
753         mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
754         int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
755         int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
756         mEventGeometry.setHourHeight(mCellHeight);
757
758         // Create an off-screen bitmap that we can draw into.
759         mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
760         if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
761                 mBitmapHeight > 0) {
762             if (mBitmap != null) {
763                 mBitmap.recycle();
764             }
765             mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
766             mCanvas = new Canvas(mBitmap);
767         }
768         mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
769
770         if (mFirstHour == -1) {
771             initFirstHour();
772             mFirstHourOffset = 0;
773         }
774
775         // When we change the base date, the number of all-day events may
776         // change and that changes the cell height.  When we switch dates,
777         // we use the mFirstHourOffset from the previous view, but that may
778         // be too large for the new view if the cell height is smaller.
779         if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
780             mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
781         }
782         mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
783
784         int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
785         //When we get new events we don't want to dismiss the popup unless the event changes
786         if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
787             mPopup.dismiss();
788         }
789         mPopup.setWidth(eventAreaWidth - 20);
790         mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
791     }
792
793     /**
794      * Initialize the state for another view.  The given view is one that has
795      * its own bitmap and will use an animation to replace the current view.
796      * The current view and new view are either both Week views or both Day
797      * views.  They differ in their base date.
798      *
799      * @param view the view to initialize.
800      */
801     private void initView(CalendarView view) {
802         view.mSelectionHour = mSelectionHour;
803         view.mSelectedEvents.clear();
804         view.mComputeSelectedEvents = true;
805         view.mFirstHour = mFirstHour;
806         view.mFirstHourOffset = mFirstHourOffset;
807         view.remeasure(getWidth(), getHeight());
808
809         view.mSelectedEvent = null;
810         view.mPrevSelectedEvent = null;
811         view.mStartDay = mStartDay;
812         if (view.mEvents.size() > 0) {
813             view.mSelectionAllDay = mSelectionAllDay;
814         } else {
815             view.mSelectionAllDay = false;
816         }
817
818         // Redraw the screen so that the selection box will be redrawn.  We may
819         // have scrolled to a different part of the day in some other view
820         // so the selection box in this view may no longer be visible.
821         view.mRedrawScreen = true;
822         view.recalc();
823     }
824
825     /**
826      * Switch to another view based on what was selected (an event or a free
827      * slot) and how it was selected (by touch or by trackball).
828      *
829      * @param trackBallSelection true if the selection was made using the
830      * trackball.
831      */
832     private void switchViews(boolean trackBallSelection) {
833         Event selectedEvent = mSelectedEvent;
834
835         mPopup.dismiss();
836         mLastPopupEventID = INVALID_EVENT_ID;
837         if (mNumDays > 1) {
838             // This is the Week view.
839             // With touch, we always switch to Day/Agenda View
840             // With track ball, if we selected a free slot, then create an event.
841             // If we selected a specific event, switch to EventInfo view.
842             if (trackBallSelection) {
843                 if (selectedEvent == null) {
844                     // Switch to the EditEvent view
845                     long startMillis = getSelectedTimeInMillis();
846                     long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
847                     Intent intent = new Intent(Intent.ACTION_VIEW);
848                     intent.setClassName(mParentActivity, EditEvent.class.getName());
849                     intent.putExtra(EVENT_BEGIN_TIME, startMillis);
850                     intent.putExtra(EVENT_END_TIME, endMillis);
851                     mParentActivity.startActivity(intent);
852                 } else {
853                     // Switch to the EventInfo view
854                     Intent intent = new Intent(Intent.ACTION_VIEW);
855                     Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
856                             selectedEvent.id);
857                     intent.setData(eventUri);
858                     intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
859                     intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
860                     intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
861                     mParentActivity.startActivity(intent);
862                 }
863             } else {
864                 // This was a touch selection.  If the touch selected a single
865                 // unambiguous event, then view that event.  Otherwise go to
866                 // Day/Agenda view.
867                 if (mSelectedEvents.size() == 1) {
868                     // Switch to the EventInfo view
869                     Intent intent = new Intent(Intent.ACTION_VIEW);
870                     Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
871                             selectedEvent.id);
872                     intent.setData(eventUri);
873                     intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
874                     intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
875                     intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
876                     mParentActivity.startActivity(intent);
877                 } else {
878                     // Switch to the Day/Agenda view.
879                     long millis = getSelectedTimeInMillis();
880                     Utils.startActivity(mParentActivity, mDetailedView, millis);
881                 }
882             }
883         } else {
884             // This is the Day view.
885             // If we selected a free slot, then create an event.
886             // If we selected an event, then go to the EventInfo view.
887             if (selectedEvent == null) {
888                 // Switch to the EditEvent view
889                 long startMillis = getSelectedTimeInMillis();
890                 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
891                 Intent intent = new Intent(Intent.ACTION_VIEW);
892                 intent.setClassName(mParentActivity, EditEvent.class.getName());
893                 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
894                 intent.putExtra(EVENT_END_TIME, endMillis);
895                 mParentActivity.startActivity(intent);
896             } else {
897                 // Switch to the EventInfo view
898                 Intent intent = new Intent(Intent.ACTION_VIEW);
899                 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
900                 intent.setData(eventUri);
901                 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
902                 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
903                 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
904                 mParentActivity.startActivity(intent);
905             }
906         }
907     }
908
909     @Override
910     public boolean onKeyUp(int keyCode, KeyEvent event) {
911         mScrolling = false;
912         long duration = event.getEventTime() - event.getDownTime();
913
914         switch (keyCode) {
915             case KeyEvent.KEYCODE_DPAD_CENTER:
916                 if (mSelectionMode == SELECTION_HIDDEN) {
917                     // Don't do anything unless the selection is visible.
918                     break;
919                 }
920
921                 if (mSelectionMode == SELECTION_PRESSED) {
922                     // This was the first press when there was nothing selected.
923                     // Change the selection from the "pressed" state to the
924                     // the "selected" state.  We treat short-press and
925                     // long-press the same here because nothing was selected.
926                     mSelectionMode = SELECTION_SELECTED;
927                     mRedrawScreen = true;
928                     invalidate();
929                     break;
930                 }
931
932                 // Check the duration to determine if this was a short press
933                 if (duration < ViewConfiguration.getLongPressTimeout()) {
934                     switchViews(true /* trackball */);
935                 } else {
936                     mSelectionMode = SELECTION_LONGPRESS;
937                     mRedrawScreen = true;
938                     invalidate();
939                     performLongClick();
940                 }
941                 break;
942             case KeyEvent.KEYCODE_BACK:
943                 if (event.isTracking() && !event.isCanceled()) {
944                     mPopup.dismiss();
945                     mParentActivity.finish();
946                     return true;
947                 }
948                 break;
949         }
950         return super.onKeyUp(keyCode, event);
951     }
952
953     @Override
954     public boolean onKeyDown(int keyCode, KeyEvent event) {
955         if (mSelectionMode == SELECTION_HIDDEN) {
956             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
957                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
958                     || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
959                 // Display the selection box but don't move or select it
960                 // on this key press.
961                 mSelectionMode = SELECTION_SELECTED;
962                 mRedrawScreen = true;
963                 invalidate();
964                 return true;
965             } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
966                 // Display the selection box but don't select it
967                 // on this key press.
968                 mSelectionMode = SELECTION_PRESSED;
969                 mRedrawScreen = true;
970                 invalidate();
971                 return true;
972             }
973         }
974
975         mSelectionMode = SELECTION_SELECTED;
976         mScrolling = false;
977         boolean redraw;
978         int selectionDay = mSelectionDay;
979
980         switch (keyCode) {
981         case KeyEvent.KEYCODE_DEL:
982             // Delete the selected event, if any
983             Event selectedEvent = mSelectedEvent;
984             if (selectedEvent == null) {
985                 return false;
986             }
987             mPopup.dismiss();
988             mLastPopupEventID = INVALID_EVENT_ID;
989
990             long begin = selectedEvent.startMillis;
991             long end = selectedEvent.endMillis;
992             long id = selectedEvent.id;
993             mDeleteEventHelper.delete(begin, end, id, -1);
994             return true;
995         case KeyEvent.KEYCODE_ENTER:
996             switchViews(true /* trackball or keyboard */);
997             return true;
998         case KeyEvent.KEYCODE_BACK:
999             if (event.getRepeatCount() == 0) {
1000                 event.startTracking();
1001                 return true;
1002             }
1003             return super.onKeyDown(keyCode, event);
1004         case KeyEvent.KEYCODE_DPAD_LEFT:
1005             if (mSelectedEvent != null) {
1006                 mSelectedEvent = mSelectedEvent.nextLeft;
1007             }
1008             if (mSelectedEvent == null) {
1009                 mLastPopupEventID = INVALID_EVENT_ID;
1010                 selectionDay -= 1;
1011             }
1012             redraw = true;
1013             break;
1014
1015         case KeyEvent.KEYCODE_DPAD_RIGHT:
1016             if (mSelectedEvent != null) {
1017                 mSelectedEvent = mSelectedEvent.nextRight;
1018             }
1019             if (mSelectedEvent == null) {
1020                 mLastPopupEventID = INVALID_EVENT_ID;
1021                 selectionDay += 1;
1022             }
1023             redraw = true;
1024             break;
1025
1026         case KeyEvent.KEYCODE_DPAD_UP:
1027             if (mSelectedEvent != null) {
1028                 mSelectedEvent = mSelectedEvent.nextUp;
1029             }
1030             if (mSelectedEvent == null) {
1031                 mLastPopupEventID = INVALID_EVENT_ID;
1032                 if (!mSelectionAllDay) {
1033                     mSelectionHour -= 1;
1034                     adjustHourSelection();
1035                     mSelectedEvents.clear();
1036                     mComputeSelectedEvents = true;
1037                 }
1038             }
1039             redraw = true;
1040             break;
1041
1042         case KeyEvent.KEYCODE_DPAD_DOWN:
1043             if (mSelectedEvent != null) {
1044                 mSelectedEvent = mSelectedEvent.nextDown;
1045             }
1046             if (mSelectedEvent == null) {
1047                 mLastPopupEventID = INVALID_EVENT_ID;
1048                 if (mSelectionAllDay) {
1049                     mSelectionAllDay = false;
1050                 } else {
1051                     mSelectionHour++;
1052                     adjustHourSelection();
1053                     mSelectedEvents.clear();
1054                     mComputeSelectedEvents = true;
1055                 }
1056             }
1057             redraw = true;
1058             break;
1059
1060         default:
1061             return super.onKeyDown(keyCode, event);
1062         }
1063
1064         if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1065             boolean forward;
1066             CalendarView view = mParentActivity.getNextView();
1067             Time date = view.mBaseDate;
1068             date.set(mBaseDate);
1069             if (selectionDay < mFirstJulianDay) {
1070                 date.monthDay -= mNumDays;
1071                 forward = false;
1072             } else {
1073                 date.monthDay += mNumDays;
1074                 forward = true;
1075             }
1076             date.normalize(true /* ignore isDst */);
1077             view.mSelectionDay = selectionDay;
1078
1079             initView(view);
1080             mTitleTextView.setText(view.mDateRange);
1081             mParentActivity.switchViews(forward, 0, 0);
1082             return true;
1083         }
1084         mSelectionDay = selectionDay;
1085         mSelectedEvents.clear();
1086         mComputeSelectedEvents = true;
1087
1088         if (redraw) {
1089             mRedrawScreen = true;
1090             invalidate();
1091             return true;
1092         }
1093
1094         return super.onKeyDown(keyCode, event);
1095     }
1096
1097     // This is called after scrolling stops to move the selected hour
1098     // to the visible part of the screen.
1099     private void resetSelectedHour() {
1100         if (mSelectionHour < mFirstHour + 1) {
1101             mSelectionHour = mFirstHour + 1;
1102             mSelectedEvent = null;
1103             mSelectedEvents.clear();
1104             mComputeSelectedEvents = true;
1105         } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1106             mSelectionHour = mFirstHour + mNumHours - 3;
1107             mSelectedEvent = null;
1108             mSelectedEvents.clear();
1109             mComputeSelectedEvents = true;
1110         }
1111     }
1112
1113     private void initFirstHour() {
1114         mFirstHour = mSelectionHour - mNumHours / 2;
1115         if (mFirstHour < 0) {
1116             mFirstHour = 0;
1117         } else if (mFirstHour + mNumHours > 24) {
1118             mFirstHour = 24 - mNumHours;
1119         }
1120     }
1121
1122     /**
1123      * Recomputes the first full hour that is visible on screen after the
1124      * screen is scrolled.
1125      */
1126     private void computeFirstHour() {
1127         // Compute the first full hour that is visible on screen
1128         mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1129         mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1130     }
1131
1132     private void adjustHourSelection() {
1133         if (mSelectionHour < 0) {
1134             mSelectionHour = 0;
1135             if (mMaxAllDayEvents > 0) {
1136                 mPrevSelectedEvent = null;
1137                 mSelectionAllDay = true;
1138             }
1139         }
1140
1141         if (mSelectionHour > 23) {
1142             mSelectionHour = 23;
1143         }
1144
1145         // If the selected hour is at least 2 time slots from the top and
1146         // bottom of the screen, then don't scroll the view.
1147         if (mSelectionHour < mFirstHour + 1) {
1148             // If there are all-days events for the selected day but there
1149             // are no more normal events earlier in the day, then jump to
1150             // the all-day event area.
1151             // Exception 1: allow the user to scroll to 8am with the trackball
1152             // before jumping to the all-day event area.
1153             // Exception 2: if 12am is on screen, then allow the user to select
1154             // 12am before going up to the all-day event area.
1155             int daynum = mSelectionDay - mFirstJulianDay;
1156             if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1157                     && mFirstHour > 0 && mFirstHour < 8) {
1158                 mPrevSelectedEvent = null;
1159                 mSelectionAllDay = true;
1160                 mSelectionHour = mFirstHour + 1;
1161                 return;
1162             }
1163
1164             if (mFirstHour > 0) {
1165                 mFirstHour -= 1;
1166                 mViewStartY -= (mCellHeight + HOUR_GAP);
1167                 if (mViewStartY < 0) {
1168                     mViewStartY = 0;
1169                 }
1170                 return;
1171             }
1172         }
1173
1174         if (mSelectionHour > mFirstHour + mNumHours - 3) {
1175             if (mFirstHour < 24 - mNumHours) {
1176                 mFirstHour += 1;
1177                 mViewStartY += (mCellHeight + HOUR_GAP);
1178                 if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
1179                     mViewStartY = mBitmapHeight - mGridAreaHeight;
1180                 }
1181                 return;
1182             } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1183                 mViewStartY = mBitmapHeight - mGridAreaHeight;
1184             }
1185         }
1186     }
1187
1188     void clearCachedEvents() {
1189         mLastReloadMillis = 0;
1190     }
1191
1192     private Runnable mCancelCallback = new Runnable() {
1193         public void run() {
1194             clearCachedEvents();
1195         }
1196     };
1197
1198     void reloadEvents() {
1199         // Protect against this being called before this view has been
1200         // initialized.
1201         if (mParentActivity == null) {
1202             return;
1203         }
1204
1205         mSelectedEvent = null;
1206         mPrevSelectedEvent = null;
1207         mSelectedEvents.clear();
1208
1209         // The start date is the beginning of the week at 12am
1210         Time weekStart = new Time();
1211         weekStart.set(mBaseDate);
1212         weekStart.hour = 0;
1213         weekStart.minute = 0;
1214         weekStart.second = 0;
1215         long millis = weekStart.normalize(true /* ignore isDst */);
1216
1217         // Avoid reloading events unnecessarily.
1218         if (millis == mLastReloadMillis) {
1219             return;
1220         }
1221         mLastReloadMillis = millis;
1222
1223         // load events in the background
1224         mParentActivity.startProgressSpinner();
1225         final ArrayList<Event> events = new ArrayList<Event>();
1226         mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
1227             public void run() {
1228                 mEvents = events;
1229                 mRemeasure = true;
1230                 mRedrawScreen = true;
1231                 mComputeSelectedEvents = true;
1232                 recalc();
1233                 mParentActivity.stopProgressSpinner();
1234                 invalidate();
1235             }
1236         }, mCancelCallback);
1237     }
1238
1239     @Override
1240     protected void onDraw(Canvas canvas) {
1241         if (mRemeasure) {
1242             remeasure(getWidth(), getHeight());
1243             mRemeasure = false;
1244         }
1245
1246         if (mRedrawScreen && mCanvas != null) {
1247             doDraw(mCanvas);
1248             mRedrawScreen = false;
1249         }
1250
1251         if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1252             canvas.save();
1253             if (mViewStartX > 0) {
1254                 canvas.translate(mViewWidth - mViewStartX, 0);
1255             } else {
1256                 canvas.translate(-(mViewWidth + mViewStartX), 0);
1257             }
1258             CalendarView nextView = mParentActivity.getNextView();
1259
1260             // Prevent infinite recursive calls to onDraw().
1261             nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
1262
1263             nextView.onDraw(canvas);
1264             canvas.restore();
1265             canvas.save();
1266             canvas.translate(-mViewStartX, 0);
1267         }
1268
1269         if (mBitmap != null) {
1270             drawCalendarView(canvas);
1271         }
1272
1273         // Draw the fixed areas (that don't scroll) directly to the canvas.
1274         drawAfterScroll(canvas);
1275         mComputeSelectedEvents = false;
1276
1277         if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1278             canvas.restore();
1279         }
1280     }
1281
1282     private void drawCalendarView(Canvas canvas) {
1283
1284         // Copy the scrollable region from the big bitmap to the canvas.
1285         Rect src = mSrcRect;
1286         Rect dest = mDestRect;
1287
1288         src.top = mViewStartY;
1289         src.bottom = mViewStartY + mGridAreaHeight;
1290         src.left = 0;
1291         src.right = mViewWidth;
1292
1293         dest.top = mFirstCell;
1294         dest.bottom = mViewHeight;
1295         dest.left = 0;
1296         dest.right = mViewWidth;
1297
1298         canvas.save();
1299         canvas.clipRect(dest);
1300         canvas.drawColor(0, PorterDuff.Mode.CLEAR);
1301         canvas.drawBitmap(mBitmap, src, dest, null);
1302         canvas.restore();
1303     }
1304
1305     private void drawAfterScroll(Canvas canvas) {
1306         Paint p = mPaint;
1307         Rect r = mRect;
1308
1309         if (mMaxAllDayEvents != 0) {
1310             drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
1311             drawUpperLeftCorner(r, canvas, p);
1312         }
1313
1314         if (mNumDays > 1) {
1315             drawDayHeaderLoop(r, canvas, p);
1316         }
1317
1318         // Draw the AM and PM indicators if we're in 12 hour mode
1319         if (!mIs24HourFormat) {
1320             drawAmPm(canvas, p);
1321         }
1322
1323         // Update the popup window showing the event details, but only if
1324         // we are not scrolling and we have focus.
1325         if (!mScrolling && isFocused()) {
1326             updateEventDetails();
1327         }
1328     }
1329
1330     // This isn't really the upper-left corner.  It's the square area just
1331     // below the upper-left corner, above the hours and to the left of the
1332     // all-day area.
1333     private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
1334         p.setColor(mCalendarHourBackground);
1335         r.top = mBannerPlusMargin;
1336         r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1337         r.left = 0;
1338         r.right = mHoursWidth;
1339         canvas.drawRect(r, p);
1340     }
1341
1342     private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
1343         // Draw the horizontal day background banner
1344         p.setColor(mCalendarDateBannerBackground);
1345         r.top = 0;
1346         r.bottom = mBannerPlusMargin;
1347         r.left = 0;
1348         r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
1349         canvas.drawRect(r, p);
1350
1351         // Fill the extra space on the right side with the default background
1352         r.left = r.right;
1353         r.right = mViewWidth;
1354         p.setColor(mCalendarGridAreaBackground);
1355         canvas.drawRect(r, p);
1356
1357         // Draw a highlight on the selected day (if any), but only if we are
1358         // displaying more than one day.
1359         if (mSelectionMode != SELECTION_HIDDEN) {
1360             if (mNumDays > 1) {
1361                 p.setColor(mCalendarDateSelected);
1362                 r.top = 0;
1363                 r.bottom = mBannerPlusMargin;
1364                 int daynum = mSelectionDay - mFirstJulianDay;
1365                 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1366                 r.right = r.left + mCellWidth;
1367                 canvas.drawRect(r, p);
1368             }
1369         }
1370
1371         p.setTextSize(NORMAL_FONT_SIZE);
1372         p.setTextAlign(Paint.Align.CENTER);
1373         int x = mHoursWidth;
1374         int deltaX = mCellWidth + DAY_GAP;
1375         int cell = mFirstJulianDay;
1376
1377         String[] dayNames;
1378         if (mDateStrWidth < mCellWidth) {
1379             dayNames = mDayStrs;
1380         } else {
1381             dayNames = mDayStrs2Letter;
1382         }
1383
1384         p.setTypeface(mBold);
1385         p.setAntiAlias(true);
1386         for (int day = 0; day < mNumDays; day++, cell++) {
1387             drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
1388             x += deltaX;
1389         }
1390     }
1391
1392     private void drawAmPm(Canvas canvas, Paint p) {
1393         p.setColor(mCalendarAmPmLabel);
1394         p.setTextSize(AMPM_FONT_SIZE);
1395         p.setTypeface(mBold);
1396         p.setAntiAlias(true);
1397         mPaint.setTextAlign(Paint.Align.RIGHT);
1398         String text = mAmString;
1399         if (mFirstHour >= 12) {
1400             text = mPmString;
1401         }
1402         int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
1403         int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1404         canvas.drawText(text, right, y, p);
1405
1406         if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
1407             // Also draw the "PM"
1408             text = mPmString;
1409             y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
1410                     + 2 * mHoursTextHeight + HOUR_GAP;
1411             canvas.drawText(text, right, y, p);
1412         }
1413     }
1414
1415     private void drawCurrentTimeMarker(int top, Canvas canvas, Paint p) {
1416         top -= CURRENT_TIME_MARKER_HEIGHT / 2;
1417         p.setColor(mCurrentTimeMarkerColor);
1418         Paint.Style oldStyle = p.getStyle();
1419         p.setStyle(Paint.Style.STROKE);
1420         p.setStrokeWidth(2.0f);
1421         Path mCurrentTimeMarker = mPath;
1422         mCurrentTimeMarker.reset();
1423         mCurrentTimeMarker.moveTo(0, top);
1424         mCurrentTimeMarker.lineTo(0, CURRENT_TIME_MARKER_HEIGHT + top);
1425         mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_INNER_WIDTH, CURRENT_TIME_MARKER_HEIGHT + top);
1426         mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_WIDTH, CURRENT_TIME_MARKER_HEIGHT / 2 + top);
1427         mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_INNER_WIDTH, top);
1428         mCurrentTimeMarker.lineTo(0, top);
1429         canvas.drawPath(mCurrentTimeMarker, p);
1430         p.setStyle(oldStyle);
1431     }
1432
1433     private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) {
1434         //Do a white outline so it'll show up on a red event
1435         p.setColor(mCurrentTimeMarkerBorderColor);
1436         r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH;
1437         r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2 + CURRENT_TIME_LINE_BORDER_WIDTH;
1438         r.left = left + CURRENT_TIME_LINE_SIDE_BUFFER;
1439         r.right = r.left + mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER;
1440         canvas.drawRect(r, p);
1441         //Then draw the red line
1442         p.setColor(mCurrentTimeMarkerColor);
1443         r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
1444         r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
1445         canvas.drawRect(r, p);
1446     }
1447
1448     private void doDraw(Canvas canvas) {
1449         Paint p = mPaint;
1450         Rect r = mRect;
1451         int lineY = mCurrentTime.hour*(mCellHeight + HOUR_GAP)
1452             + ((mCurrentTime.minute * mCellHeight) / 60)
1453             + 1;
1454
1455         drawGridBackground(r, canvas, p);
1456         drawHours(r, canvas, p);
1457
1458         // Draw each day
1459         int x = mHoursWidth;
1460         int deltaX = mCellWidth + DAY_GAP;
1461         int cell = mFirstJulianDay;
1462         for (int day = 0; day < mNumDays; day++, cell++) {
1463             drawEvents(cell, x, HOUR_GAP, canvas, p);
1464             //If this is today
1465             if(cell == mTodayJulianDay) {
1466                 //And the current time shows up somewhere on the screen
1467                 if(lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
1468                     //draw both the marker and the line
1469                     drawCurrentTimeMarker(lineY, canvas, p);
1470                     drawCurrentTimeLine(r, x, lineY, canvas, p);
1471                 }
1472             }
1473             x += deltaX;
1474         }
1475     }
1476
1477     private void drawHours(Rect r, Canvas canvas, Paint p) {
1478         // Draw the background for the hour labels
1479         p.setColor(mCalendarHourBackground);
1480         r.top = 0;
1481         r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
1482         r.left = 0;
1483         r.right = mHoursWidth;
1484         canvas.drawRect(r, p);
1485
1486         // Fill the bottom left corner with the default grid background
1487         r.top = r.bottom;
1488         r.bottom = mBitmapHeight;
1489         p.setColor(mCalendarGridAreaBackground);
1490         canvas.drawRect(r, p);
1491
1492         // Draw a highlight on the selected hour (if needed)
1493         if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
1494             p.setColor(mCalendarHourSelected);
1495             r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1496             r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
1497             r.left = 0;
1498             r.right = mHoursWidth;
1499             canvas.drawRect(r, p);
1500
1501             // Also draw the highlight on the grid
1502             p.setColor(mCalendarGridAreaSelected);
1503             int daynum = mSelectionDay - mFirstJulianDay;
1504             r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1505             r.right = r.left + mCellWidth;
1506             canvas.drawRect(r, p);
1507
1508             // Draw a border around the highlighted grid hour.
1509             Path path = mPath;
1510             r.top += HOUR_GAP;
1511             r.bottom -= HOUR_GAP;
1512             path.reset();
1513             path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
1514             canvas.drawPath(path, mSelectionPaint);
1515             saveSelectionPosition(r.left, r.top, r.right, r.bottom);
1516         }
1517
1518         p.setColor(mCalendarHourLabel);
1519         p.setTextSize(HOURS_FONT_SIZE);
1520         p.setTypeface(mBold);
1521         p.setTextAlign(Paint.Align.RIGHT);
1522         p.setAntiAlias(true);
1523
1524         int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1525         int y = HOUR_GAP + mHoursTextHeight;
1526
1527         for (int i = 0; i < 24; i++) {
1528             String time = mHourStrs[i];
1529             canvas.drawText(time, right, y, p);
1530             y += mCellHeight + HOUR_GAP;
1531         }
1532     }
1533
1534     private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
1535         float xCenter = x + mCellWidth / 2.0f;
1536
1537         if (Utils.isSaturday(day, mStartDay)) {
1538             p.setColor(mWeek_saturdayColor);
1539         } else if (Utils.isSunday(day, mStartDay)) {
1540             p.setColor(mWeek_sundayColor);
1541         } else {
1542             p.setColor(mCalendarDateBannerTextColor);
1543         }
1544
1545         int dateNum = mFirstDate + day;
1546         if (dateNum > mMonthLength) {
1547             dateNum -= mMonthLength;
1548         }
1549
1550         String dateNumStr;
1551         // Add a leading zero if the date is a single digit
1552         if (dateNum < 10) {
1553             dateNumStr = "0" + dateNum;
1554         } else {
1555             dateNumStr = String.valueOf(dateNum);
1556         }
1557
1558         DayHeader header = dayHeaders[day];
1559         if (header == null || header.cell != cell) {
1560             // The day header string is regenerated on every draw during drag and fling animation.
1561             // Caching day header since formatting the string takes surprising long time.
1562
1563             dayHeaders[day] = new DayHeader();
1564             dayHeaders[day].cell = cell;
1565             dayHeaders[day].dateString = getResources().getString(
1566                     R.string.weekday_day, dateStr, dateNumStr);
1567         }
1568         dateStr = dayHeaders[day].dateString;
1569
1570         float y = mBannerPlusMargin - 7;
1571         canvas.drawText(dateStr, xCenter, y, p);
1572     }
1573
1574     private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
1575         Paint.Style savedStyle = p.getStyle();
1576
1577         // Clear the background
1578         p.setColor(mCalendarGridAreaBackground);
1579         r.top = 0;
1580         r.bottom = mBitmapHeight;
1581         r.left = 0;
1582         r.right = mViewWidth;
1583         canvas.drawRect(r, p);
1584
1585         // Draw the horizontal grid lines
1586         p.setColor(mCalendarGridLineHorizontalColor);
1587         p.setStyle(Style.STROKE);
1588         p.setStrokeWidth(0);
1589         p.setAntiAlias(false);
1590         float startX = mHoursWidth;
1591         float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
1592         float y = 0;
1593         float deltaY = mCellHeight + HOUR_GAP;
1594         for (int hour = 0; hour <= 24; hour++) {
1595             canvas.drawLine(startX, y, stopX, y, p);
1596             y += deltaY;
1597         }
1598
1599         // Draw the vertical grid lines
1600         p.setColor(mCalendarGridLineVerticalColor);
1601         float startY = 0;
1602         float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
1603         float deltaX = mCellWidth + DAY_GAP;
1604         float x = mHoursWidth + mCellWidth;
1605         for (int day = 0; day < mNumDays; day++) {
1606             canvas.drawLine(x, startY, x, stopY, p);
1607             x += deltaX;
1608         }
1609
1610         // Restore the saved style.
1611         p.setStyle(savedStyle);
1612         p.setAntiAlias(true);
1613     }
1614
1615     Event getSelectedEvent() {
1616         if (mSelectedEvent == null) {
1617             // There is no event at the selected hour, so create a new event.
1618             return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1619                     getSelectedMinutesSinceMidnight());
1620         }
1621         return mSelectedEvent;
1622     }
1623
1624     boolean isEventSelected() {
1625         return (mSelectedEvent != null);
1626     }
1627
1628     Event getNewEvent() {
1629         return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1630                 getSelectedMinutesSinceMidnight());
1631     }
1632
1633     static Event getNewEvent(int julianDay, long utcMillis,
1634             int minutesSinceMidnight) {
1635         Event event = Event.newInstance();
1636         event.startDay = julianDay;
1637         event.endDay = julianDay;
1638         event.startMillis = utcMillis;
1639         event.endMillis = event.startMillis + MILLIS_PER_HOUR;
1640         event.startTime = minutesSinceMidnight;
1641         event.endTime = event.startTime + MINUTES_PER_HOUR;
1642         return event;
1643     }
1644
1645     private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
1646         float maxWidthF = 0.0f;
1647
1648         int len = strings.length;
1649         for (int i = 0; i < len; i++) {
1650             float width = p.measureText(strings[i]);
1651             maxWidthF = Math.max(width, maxWidthF);
1652         }
1653         int maxWidth = (int) (maxWidthF + 0.5);
1654         if (maxWidth < currentMax) {
1655             maxWidth = currentMax;
1656         }
1657         return maxWidth;
1658     }
1659
1660     private void saveSelectionPosition(float left, float top, float right, float bottom) {
1661         mPrevBox.left = (int) left;
1662         mPrevBox.right = (int) right;
1663         mPrevBox.top = (int) top;
1664         mPrevBox.bottom = (int) bottom;
1665     }
1666
1667     private Rect getCurrentSelectionPosition() {
1668         Rect box = new Rect();
1669         box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1670         box.bottom = box.top + mCellHeight + HOUR_GAP;
1671         int daynum = mSelectionDay - mFirstJulianDay;
1672         box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1673         box.right = box.left + mCellWidth + DAY_GAP;
1674         return box;
1675     }
1676
1677     private void drawAllDayEvents(int firstDay, int numDays,
1678             Rect r, Canvas canvas, Paint p) {
1679         p.setTextSize(NORMAL_FONT_SIZE);
1680         p.setTextAlign(Paint.Align.LEFT);
1681         Paint eventTextPaint = mEventTextPaint;
1682
1683         // Draw the background for the all-day events area
1684         r.top = mBannerPlusMargin;
1685         r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1686         r.left = mHoursWidth;
1687         r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
1688         p.setColor(mCalendarAllDayBackground);
1689         canvas.drawRect(r, p);
1690
1691         // Fill the extra space on the right side with the default background
1692         r.left = r.right;
1693         r.right = mViewWidth;
1694         p.setColor(mCalendarGridAreaBackground);
1695         canvas.drawRect(r, p);
1696
1697         // Draw the vertical grid lines
1698         p.setColor(mCalendarGridLineVerticalColor);
1699         p.setStyle(Style.STROKE);
1700         p.setStrokeWidth(0);
1701         p.setAntiAlias(false);
1702         float startY = r.top;
1703         float stopY = r.bottom;
1704         float deltaX = mCellWidth + DAY_GAP;
1705         float x = mHoursWidth + mCellWidth;
1706         for (int day = 0; day <= mNumDays; day++) {
1707             canvas.drawLine(x, startY, x, stopY, p);
1708             x += deltaX;
1709         }
1710         p.setAntiAlias(true);
1711         p.setStyle(Style.FILL);
1712
1713         int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
1714         float left = mHoursWidth;
1715         int lastDay = firstDay + numDays - 1;
1716         ArrayList<Event> events = mEvents;
1717         int numEvents = events.size();
1718         float drawHeight = mAllDayHeight;
1719         float numRectangles = mMaxAllDayEvents;
1720         for (int i = 0; i < numEvents; i++) {
1721             Event event = events.get(i);
1722             if (!event.allDay)
1723                 continue;
1724             int startDay = event.startDay;
1725             int endDay = event.endDay;
1726             if (startDay > lastDay || endDay < firstDay)
1727                 continue;
1728             if (startDay < firstDay)
1729                 startDay = firstDay;
1730             if (endDay > lastDay)
1731                 endDay = lastDay;
1732             int startIndex = startDay - firstDay;
1733             int endIndex = endDay - firstDay;
1734             float height = drawHeight / numRectangles;
1735
1736             // Prevent a single event from getting too big
1737             if (height > MAX_ALLDAY_EVENT_HEIGHT) {
1738                 height = MAX_ALLDAY_EVENT_HEIGHT;
1739             }
1740
1741             // Leave a one-pixel space between the vertical day lines and the
1742             // event rectangle.
1743             event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
1744             event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
1745             event.top = y + height * event.getColumn();
1746
1747             // Multiply the height by 0.9 to leave a little gap between events
1748             event.bottom = event.top + height * 0.9f;
1749
1750             RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1751             drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1752
1753             // Check if this all-day event intersects the selected day
1754             if (mSelectionAllDay && mComputeSelectedEvents) {
1755                 if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
1756                     mSelectedEvents.add(event);
1757                 }
1758             }
1759         }
1760
1761         if (mSelectionAllDay) {
1762             // Compute the neighbors for the list of all-day events that
1763             // intersect the selected day.
1764             computeAllDayNeighbors();
1765             if (mSelectedEvent != null) {
1766                 Event event = mSelectedEvent;
1767                 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1768                 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1769             }
1770
1771             // Draw the highlight on the selected all-day area
1772             float top = mBannerPlusMargin + 1;
1773             float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
1774             int daynum = mSelectionDay - mFirstJulianDay;
1775             left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
1776             float right = left + mCellWidth + DAY_GAP - 1;
1777             if (mNumDays == 1) {
1778                 // The Day view doesn't have a vertical line on the right.
1779                 right -= 1;
1780             }
1781             Path path = mPath;
1782             path.reset();
1783             path.addRect(left, top, right, bottom, Direction.CW);
1784             canvas.drawPath(path, mSelectionPaint);
1785
1786             // Set the selection position to zero so that when we move down
1787             // to the normal event area, we will highlight the topmost event.
1788             saveSelectionPosition(0f, 0f, 0f, 0f);
1789         }
1790     }
1791
1792     private void computeAllDayNeighbors() {
1793         int len = mSelectedEvents.size();
1794         if (len == 0 || mSelectedEvent != null) {
1795             return;
1796         }
1797
1798         // First, clear all the links
1799         for (int ii = 0; ii < len; ii++) {
1800             Event ev = mSelectedEvents.get(ii);
1801             ev.nextUp = null;
1802             ev.nextDown = null;
1803             ev.nextLeft = null;
1804             ev.nextRight = null;
1805         }
1806
1807         // For each event in the selected event list "mSelectedEvents", find
1808         // its neighbors in the up and down directions.  This could be done
1809         // more efficiently by sorting on the Event.getColumn() field, but
1810         // the list is expected to be very small.
1811
1812         // Find the event in the same row as the previously selected all-day
1813         // event, if any.
1814         int startPosition = -1;
1815         if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
1816             startPosition = mPrevSelectedEvent.getColumn();
1817         }
1818         int maxPosition = -1;
1819         Event startEvent = null;
1820         Event maxPositionEvent = null;
1821         for (int ii = 0; ii < len; ii++) {
1822             Event ev = mSelectedEvents.get(ii);
1823             int position = ev.getColumn();
1824             if (position == startPosition) {
1825                 startEvent = ev;
1826             } else if (position > maxPosition) {
1827                 maxPositionEvent = ev;
1828                 maxPosition = position;
1829             }
1830             for (int jj = 0; jj < len; jj++) {
1831                 if (jj == ii) {
1832                     continue;
1833                 }
1834                 Event neighbor = mSelectedEvents.get(jj);
1835                 int neighborPosition = neighbor.getColumn();
1836                 if (neighborPosition == position - 1) {
1837                     ev.nextUp = neighbor;
1838                 } else if (neighborPosition == position + 1) {
1839                     ev.nextDown = neighbor;
1840                 }
1841             }
1842         }
1843         if (startEvent != null) {
1844             mSelectedEvent = startEvent;
1845         } else {
1846             mSelectedEvent = maxPositionEvent;
1847         }
1848     }
1849
1850     RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
1851         // If this event is selected, then use the selection color
1852         if (mSelectedEvent == event) {
1853             // Also, remember the last selected event that we drew
1854             mPrevSelectedEvent = event;
1855             p.setColor(mSelectionColor);
1856             eventTextPaint.setColor(mSelectedEventTextColor);
1857         } else {
1858             // Use the normal color for all-day events
1859             p.setColor(event.color);
1860             eventTextPaint.setColor(mEventTextColor);
1861         }
1862
1863         RectF rf = mRectF;
1864         rf.top = event.top;
1865         rf.bottom = event.bottom;
1866         rf.left = event.left;
1867         rf.right = event.right;
1868         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
1869
1870         rf.left += 2;
1871         rf.right -= 2;
1872         return rf;
1873     }
1874
1875     private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
1876         Paint eventTextPaint = mEventTextPaint;
1877         int cellWidth = mCellWidth;
1878         int cellHeight = mCellHeight;
1879
1880         // Use the selected hour as the selection region
1881         Rect selectionArea = mRect;
1882         selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
1883         selectionArea.bottom = selectionArea.top + cellHeight;
1884         selectionArea.left = left;
1885         selectionArea.right = selectionArea.left + cellWidth;
1886
1887         ArrayList<Event> events = mEvents;
1888         int numEvents = events.size();
1889         EventGeometry geometry = mEventGeometry;
1890
1891         for (int i = 0; i < numEvents; i++) {
1892             Event event = events.get(i);
1893             if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
1894                 continue;
1895             }
1896
1897             if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
1898                     && geometry.eventIntersectsSelection(event, selectionArea)) {
1899                 mSelectedEvents.add(event);
1900             }
1901
1902             RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
1903             drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1904         }
1905
1906         if (date == mSelectionDay && !mSelectionAllDay && isFocused()
1907                 && mSelectionMode != SELECTION_HIDDEN) {
1908             computeNeighbors();
1909             if (mSelectedEvent != null) {
1910                 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
1911                 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1912             }
1913         }
1914     }
1915
1916     // Computes the "nearest" neighbor event in four directions (left, right,
1917     // up, down) for each of the events in the mSelectedEvents array.
1918     private void computeNeighbors() {
1919         int len = mSelectedEvents.size();
1920         if (len == 0 || mSelectedEvent != null) {
1921             return;
1922         }
1923
1924         // First, clear all the links
1925         for (int ii = 0; ii < len; ii++) {
1926             Event ev = mSelectedEvents.get(ii);
1927             ev.nextUp = null;
1928             ev.nextDown = null;
1929             ev.nextLeft = null;
1930             ev.nextRight = null;
1931         }
1932
1933         Event startEvent = mSelectedEvents.get(0);
1934         int startEventDistance1 = 100000;  // any large number
1935         int startEventDistance2 = 100000;  // any large number
1936         int prevLocation = FROM_NONE;
1937         int prevTop;
1938         int prevBottom;
1939         int prevLeft;
1940         int prevRight;
1941         int prevCenter = 0;
1942         Rect box = getCurrentSelectionPosition();
1943         if (mPrevSelectedEvent != null) {
1944             prevTop = (int) mPrevSelectedEvent.top;
1945             prevBottom = (int) mPrevSelectedEvent.bottom;
1946             prevLeft = (int) mPrevSelectedEvent.left;
1947             prevRight = (int) mPrevSelectedEvent.right;
1948             // Check if the previously selected event intersects the previous
1949             // selection box.  (The previously selected event may be from a
1950             // much older selection box.)
1951             if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
1952                     || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
1953                 mPrevSelectedEvent = null;
1954                 prevTop = mPrevBox.top;
1955                 prevBottom = mPrevBox.bottom;
1956                 prevLeft = mPrevBox.left;
1957                 prevRight = mPrevBox.right;
1958             } else {
1959                 // Clip the top and bottom to the previous selection box.
1960                 if (prevTop < mPrevBox.top) {
1961                     prevTop = mPrevBox.top;
1962                 }
1963                 if (prevBottom > mPrevBox.bottom) {
1964                     prevBottom = mPrevBox.bottom;
1965                 }
1966             }
1967         } else {
1968             // Just use the previously drawn selection box
1969             prevTop = mPrevBox.top;
1970             prevBottom = mPrevBox.bottom;
1971             prevLeft = mPrevBox.left;
1972             prevRight = mPrevBox.right;
1973         }
1974
1975         // Figure out where we came from and compute the center of that area.
1976         if (prevLeft >= box.right) {
1977             // The previously selected event was to the right of us.
1978             prevLocation = FROM_RIGHT;
1979             prevCenter = (prevTop + prevBottom) / 2;
1980         } else if (prevRight <= box.left) {
1981             // The previously selected event was to the left of us.
1982             prevLocation = FROM_LEFT;
1983             prevCenter = (prevTop + prevBottom) / 2;
1984         } else if (prevBottom <= box.top) {
1985             // The previously selected event was above us.
1986             prevLocation = FROM_ABOVE;
1987             prevCenter = (prevLeft + prevRight) / 2;
1988         } else if (prevTop >= box.bottom) {
1989             // The previously selected event was below us.
1990             prevLocation = FROM_BELOW;
1991             prevCenter = (prevLeft + prevRight) / 2;
1992         }
1993
1994         // For each event in the selected event list "mSelectedEvents", search
1995         // all the other events in that list for the nearest neighbor in 4
1996         // directions.
1997         for (int ii = 0; ii < len; ii++) {
1998             Event ev = mSelectedEvents.get(ii);
1999
2000             int startTime = ev.startTime;
2001             int endTime = ev.endTime;
2002             int left = (int) ev.left;
2003             int right = (int) ev.right;
2004             int top = (int) ev.top;
2005             if (top < box.top) {
2006                 top = box.top;
2007             }
2008             int bottom = (int) ev.bottom;
2009             if (bottom > box.bottom) {
2010                 bottom = box.bottom;
2011             }
2012             if (false) {
2013                 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2014                         | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2015                 if (DateFormat.is24HourFormat(mParentActivity)) {
2016                     flags |= DateUtils.FORMAT_24HOUR;
2017                 }
2018                 String timeRange = DateUtils.formatDateRange(mParentActivity,
2019                         ev.startMillis, ev.endMillis, flags);
2020                 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
2021                         + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
2022             }
2023             int upDistanceMin = 10000;     // any large number
2024             int downDistanceMin = 10000;   // any large number
2025             int leftDistanceMin = 10000;   // any large number
2026             int rightDistanceMin = 10000;  // any large number
2027             Event upEvent = null;
2028             Event downEvent = null;
2029             Event leftEvent = null;
2030             Event rightEvent = null;
2031
2032             // Pick the starting event closest to the previously selected event,
2033             // if any.  distance1 takes precedence over distance2.
2034             int distance1 = 0;
2035             int distance2 = 0;
2036             if (prevLocation == FROM_ABOVE) {
2037                 if (left >= prevCenter) {
2038                     distance1 = left - prevCenter;
2039                 } else if (right <= prevCenter) {
2040                     distance1 = prevCenter - right;
2041                 }
2042                 distance2 = top - prevBottom;
2043             } else if (prevLocation == FROM_BELOW) {
2044                 if (left >= prevCenter) {
2045                     distance1 = left - prevCenter;
2046                 } else if (right <= prevCenter) {
2047                     distance1 = prevCenter - right;
2048                 }
2049                 distance2 = prevTop - bottom;
2050             } else if (prevLocation == FROM_LEFT) {
2051                 if (bottom <= prevCenter) {
2052                     distance1 = prevCenter - bottom;
2053                 } else if (top >= prevCenter) {
2054                     distance1 = top - prevCenter;
2055                 }
2056                 distance2 = left - prevRight;
2057             } else if (prevLocation == FROM_RIGHT) {
2058                 if (bottom <= prevCenter) {
2059                     distance1 = prevCenter - bottom;
2060                 } else if (top >= prevCenter) {
2061                     distance1 = top - prevCenter;
2062                 }
2063                 distance2 = prevLeft - right;
2064             }
2065             if (distance1 < startEventDistance1
2066                     || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
2067                 startEvent = ev;
2068                 startEventDistance1 = distance1;
2069                 startEventDistance2 = distance2;
2070             }
2071
2072             // For each neighbor, figure out if it is above or below or left
2073             // or right of me and compute the distance.
2074             for (int jj = 0; jj < len; jj++) {
2075                 if (jj == ii) {
2076                     continue;
2077                 }
2078                 Event neighbor = mSelectedEvents.get(jj);
2079                 int neighborLeft = (int) neighbor.left;
2080                 int neighborRight = (int) neighbor.right;
2081                 if (neighbor.endTime <= startTime) {
2082                     // This neighbor is entirely above me.
2083                     // If we overlap the same column, then compute the distance.
2084                     if (neighborLeft < right && neighborRight > left) {
2085                         int distance = startTime - neighbor.endTime;
2086                         if (distance < upDistanceMin) {
2087                             upDistanceMin = distance;
2088                             upEvent = neighbor;
2089                         } else if (distance == upDistanceMin) {
2090                             int center = (left + right) / 2;
2091                             int currentDistance = 0;
2092                             int currentLeft = (int) upEvent.left;
2093                             int currentRight = (int) upEvent.right;
2094                             if (currentRight <= center) {
2095                                 currentDistance = center - currentRight;
2096                             } else if (currentLeft >= center) {
2097                                 currentDistance = currentLeft - center;
2098                             }
2099
2100                             int neighborDistance = 0;
2101                             if (neighborRight <= center) {
2102                                 neighborDistance = center - neighborRight;
2103                             } else if (neighborLeft >= center) {
2104                                 neighborDistance = neighborLeft - center;
2105                             }
2106                             if (neighborDistance < currentDistance) {
2107                                 upDistanceMin = distance;
2108                                 upEvent = neighbor;
2109                             }
2110                         }
2111                     }
2112                 } else if (neighbor.startTime >= endTime) {
2113                     // This neighbor is entirely below me.
2114                     // If we overlap the same column, then compute the distance.
2115                     if (neighborLeft < right && neighborRight > left) {
2116                         int distance = neighbor.startTime - endTime;
2117                         if (distance < downDistanceMin) {
2118                             downDistanceMin = distance;
2119                             downEvent = neighbor;
2120                         } else if (distance == downDistanceMin) {
2121                             int center = (left + right) / 2;
2122                             int currentDistance = 0;
2123                             int currentLeft = (int) downEvent.left;
2124                             int currentRight = (int) downEvent.right;
2125                             if (currentRight <= center) {
2126                                 currentDistance = center - currentRight;
2127                             } else if (currentLeft >= center) {
2128                                 currentDistance = currentLeft - center;
2129                             }
2130
2131                             int neighborDistance = 0;
2132                             if (neighborRight <= center) {
2133                                 neighborDistance = center - neighborRight;
2134                             } else if (neighborLeft >= center) {
2135                                 neighborDistance = neighborLeft - center;
2136                             }
2137                             if (neighborDistance < currentDistance) {
2138                                 downDistanceMin = distance;
2139                                 downEvent = neighbor;
2140                             }
2141                         }
2142                     }
2143                 }
2144
2145                 if (neighborLeft >= right) {
2146                     // This neighbor is entirely to the right of me.
2147                     // Take the closest neighbor in the y direction.
2148                     int center = (top + bottom) / 2;
2149                     int distance = 0;
2150                     int neighborBottom = (int) neighbor.bottom;
2151                     int neighborTop = (int) neighbor.top;
2152                     if (neighborBottom <= center) {
2153                         distance = center - neighborBottom;
2154                     } else if (neighborTop >= center) {
2155                         distance = neighborTop - center;
2156                     }
2157                     if (distance < rightDistanceMin) {
2158                         rightDistanceMin = distance;
2159                         rightEvent = neighbor;
2160                     } else if (distance == rightDistanceMin) {
2161                         // Pick the closest in the x direction
2162                         int neighborDistance = neighborLeft - right;
2163                         int currentDistance = (int) rightEvent.left - right;
2164                         if (neighborDistance < currentDistance) {
2165                             rightDistanceMin = distance;
2166                             rightEvent = neighbor;
2167                         }
2168                     }
2169                 } else if (neighborRight <= left) {
2170                     // This neighbor is entirely to the left of me.
2171                     // Take the closest neighbor in the y direction.
2172                     int center = (top + bottom) / 2;
2173                     int distance = 0;
2174                     int neighborBottom = (int) neighbor.bottom;
2175                     int neighborTop = (int) neighbor.top;
2176                     if (neighborBottom <= center) {
2177                         distance = center - neighborBottom;
2178                     } else if (neighborTop >= center) {
2179                         distance = neighborTop - center;
2180                     }
2181                     if (distance < leftDistanceMin) {
2182                         leftDistanceMin = distance;
2183                         leftEvent = neighbor;
2184                     } else if (distance == leftDistanceMin) {
2185                         // Pick the closest in the x direction
2186                         int neighborDistance = left - neighborRight;
2187                         int currentDistance = left - (int) leftEvent.right;
2188                         if (neighborDistance < currentDistance) {
2189                             leftDistanceMin = distance;
2190                             leftEvent = neighbor;
2191                         }
2192                     }
2193                 }
2194             }
2195             ev.nextUp = upEvent;
2196             ev.nextDown = downEvent;
2197             ev.nextLeft = leftEvent;
2198             ev.nextRight = rightEvent;
2199         }
2200         mSelectedEvent = startEvent;
2201     }
2202
2203
2204     private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
2205
2206         int color = event.color;
2207
2208         // Fade visible boxes if event was declined.
2209         boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
2210         if (declined) {
2211             int alpha = color & 0xff000000;
2212             color &= 0x00ffffff;
2213             int red = (color & 0x00ff0000) >> 16;
2214             int green = (color & 0x0000ff00) >> 8;
2215             int blue = (color & 0x0000ff);
2216             color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
2217             color += 0x7F7F7F + alpha;
2218         }
2219
2220         // If this event is selected, then use the selection color
2221         if (mSelectedEvent == event) {
2222             if (mSelectionMode == SELECTION_PRESSED) {
2223                 // Also, remember the last selected event that we drew
2224                 mPrevSelectedEvent = event;
2225                 // box = mBoxPressed;
2226                 p.setColor(mPressedColor); // FIXME:pressed
2227                 eventTextPaint.setColor(mSelectedEventTextColor);
2228             } else if (mSelectionMode == SELECTION_SELECTED) {
2229                 // Also, remember the last selected event that we drew
2230                 mPrevSelectedEvent = event;
2231                 // box = mBoxSelected;
2232                 p.setColor(mSelectionColor);
2233                 eventTextPaint.setColor(mSelectedEventTextColor);
2234             } else if (mSelectionMode == SELECTION_LONGPRESS) {
2235                 // box = mBoxLongPressed;
2236                 p.setColor(mPressedColor); // FIXME: longpressed (maybe -- this doesn't seem to work)
2237                 eventTextPaint.setColor(mSelectedEventTextColor);
2238             } else {
2239                 p.setColor(color);
2240                 eventTextPaint.setColor(mEventTextColor);
2241             }
2242         } else {
2243             p.setColor(color);
2244             eventTextPaint.setColor(mEventTextColor);
2245         }
2246
2247
2248         RectF rf = mRectF;
2249         rf.top = event.top;
2250         rf.bottom = event.bottom;
2251         rf.left = event.left;
2252         rf.right = event.right - 1;
2253
2254         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
2255
2256         // Draw a darker border
2257         float[] hsv = new float[3];
2258         Color.colorToHSV(p.getColor(), hsv);
2259         hsv[1] = 1.0f;
2260         hsv[2] *= 0.75f;
2261         mPaintBorder.setColor(Color.HSVToColor(hsv));
2262         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
2263
2264         rf.left += 2;
2265         rf.right -= 2;
2266
2267         return rf;
2268     }
2269
2270     private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
2271
2272     // Sanitize a string before passing it to drawText or else we get little
2273     // squares. For newlines and tabs before a comma, delete the character.
2274     // Otherwise, just replace them with a space.
2275     private String drawTextSanitizer(String string) {
2276         Matcher m = drawTextSanitizerFilter.matcher(string);
2277         string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
2278         return string;
2279     }
2280
2281     private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
2282         if (!mDrawTextInEventRect) {
2283             return;
2284         }
2285
2286         float width = rf.right - rf.left;
2287         float height = rf.bottom - rf.top;
2288
2289         // Leave one pixel extra space between lines
2290         int lineHeight = mEventTextHeight + 1;
2291
2292         // If the rectangle is too small for text, then return
2293         if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
2294             return;
2295         }
2296
2297         // Truncate the event title to a known (large enough) limit
2298         String text = event.getTitleAndLocation();
2299
2300         text = drawTextSanitizer(text);
2301
2302         int len = text.length();
2303         if (len > MAX_EVENT_TEXT_LEN) {
2304             text = text.substring(0, MAX_EVENT_TEXT_LEN);
2305             len = MAX_EVENT_TEXT_LEN;
2306         }
2307
2308         // Figure out how much space the event title will take, and create a
2309         // String fragment that will fit in the rectangle.  Use multiple lines,
2310         // if available.
2311         p.getTextWidths(text, mCharWidths);
2312         String fragment = text;
2313         float top = rf.top + mEventTextAscent + topMargin;
2314         int start = 0;
2315
2316         // Leave one pixel extra space at the bottom
2317         while (start < len && height >= (lineHeight + 1)) {
2318             boolean lastLine = (height < 2 * lineHeight + 1);
2319             // Skip leading spaces at the beginning of each line
2320             do {
2321                 char c = text.charAt(start);
2322                 if (c != ' ') break;
2323                 start += 1;
2324             } while (start < len);
2325
2326             float sum = 0;
2327             int end = start;
2328             for (int ii = start; ii < len; ii++) {
2329                 char c = text.charAt(ii);
2330
2331                 // If we found the end of a word, then remember the ending
2332                 // position.
2333                 if (c == ' ') {
2334                     end = ii;
2335                 }
2336                 sum += mCharWidths[ii];
2337                 // If adding this character would exceed the width and this
2338                 // isn't the last line, then break the line at the previous
2339                 // word.  If there was no previous word, then break this word.
2340                 if (sum > width) {
2341                     if (end > start && !lastLine) {
2342                         // There was a previous word on this line.
2343                         fragment = text.substring(start, end);
2344                         start = end;
2345                         break;
2346                     }
2347
2348                     // This is the only word and it is too long to fit on
2349                     // the line (or this is the last line), so take as many
2350                     // characters of this word as will fit.
2351                     fragment = text.substring(start, ii);
2352                     start = ii;
2353                     break;
2354                 }
2355             }
2356
2357             // If sum <= width, then we can fit the rest of the text on
2358             // this line.
2359             if (sum <= width) {
2360                 fragment = text.substring(start, len);
2361                 start = len;
2362             }
2363
2364             canvas.drawText(fragment, rf.left + 1, top, p);
2365
2366             top += lineHeight;
2367             height -= lineHeight;
2368         }
2369     }
2370
2371     private void updateEventDetails() {
2372         if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
2373                 || mSelectionMode == SELECTION_LONGPRESS) {
2374             mPopup.dismiss();
2375             return;
2376         }
2377         if (mLastPopupEventID == mSelectedEvent.id) {
2378             return;
2379         }
2380
2381         mLastPopupEventID = mSelectedEvent.id;
2382
2383         // Remove any outstanding callbacks to dismiss the popup.
2384         getHandler().removeCallbacks(mDismissPopup);
2385
2386         Event event = mSelectedEvent;
2387         TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
2388         titleView.setText(event.title);
2389
2390         ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
2391         imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
2392
2393         imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
2394         imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
2395
2396         int flags;
2397         if (event.allDay) {
2398             flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
2399                     DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
2400         } else {
2401             flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
2402                     | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
2403                     | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2404         }
2405         if (DateFormat.is24HourFormat(mParentActivity)) {
2406             flags |= DateUtils.FORMAT_24HOUR;
2407         }
2408         String timeRange = DateUtils.formatDateRange(mParentActivity,
2409                 event.startMillis, event.endMillis, flags);
2410         TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
2411         timeView.setText(timeRange);
2412
2413         TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
2414         final boolean empty = TextUtils.isEmpty(event.location);
2415         whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
2416         if (!empty) whereView.setText(event.location);
2417
2418         mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
2419         postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
2420     }
2421
2422     // The following routines are called from the parent activity when certain
2423     // touch events occur.
2424
2425     void doDown(MotionEvent ev) {
2426         mTouchMode = TOUCH_MODE_DOWN;
2427         mViewStartX = 0;
2428         mOnFlingCalled = false;
2429         getHandler().removeCallbacks(mContinueScroll);
2430     }
2431
2432     void doSingleTapUp(MotionEvent ev) {
2433         int x = (int) ev.getX();
2434         int y = (int) ev.getY();
2435         Event selectedEvent = mSelectedEvent;
2436         int selectedDay = mSelectionDay;
2437         int selectedHour = mSelectionHour;
2438
2439         boolean validPosition = setSelectionFromPosition(x, y);
2440         if (!validPosition) {
2441             // return if the touch wasn't on an area of concern
2442             return;
2443         }
2444
2445         mSelectionMode = SELECTION_SELECTED;
2446         mRedrawScreen = true;
2447         invalidate();
2448
2449         boolean launchNewView = false;
2450         if (mSelectedEvent != null) {
2451             // If the tap is on an event, launch the "View event" view
2452             launchNewView = true;
2453         } else if (mSelectedEvent == null && selectedDay == mSelectionDay
2454                 && selectedHour == mSelectionHour) {
2455             // If the tap is on an already selected hour slot,
2456             // then launch the Day/Agenda view. Otherwise, just select the hour
2457             // slot.
2458             launchNewView = true;
2459         }
2460
2461         if (launchNewView) {
2462             switchViews(false /* not the trackball */);
2463         }
2464     }
2465
2466     void doLongPress(MotionEvent ev) {
2467         int x = (int) ev.getX();
2468         int y = (int) ev.getY();
2469
2470         boolean validPosition = setSelectionFromPosition(x, y);
2471         if (!validPosition) {
2472             // return if the touch wasn't on an area of concern
2473             return;
2474         }
2475
2476         mSelectionMode = SELECTION_LONGPRESS;
2477         mRedrawScreen = true;
2478         invalidate();
2479         performLongClick();
2480     }
2481
2482     void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
2483         // Use the distance from the current point to the initial touch instead
2484         // of deltaX and deltaY to avoid accumulating floating-point rounding
2485         // errors.  Also, we don't need floats, we can use ints.
2486         int distanceX = (int) e1.getX() - (int) e2.getX();
2487         int distanceY = (int) e1.getY() - (int) e2.getY();
2488
2489         // If we haven't figured out the predominant scroll direction yet,
2490         // then do it now.
2491         if (mTouchMode == TOUCH_MODE_DOWN) {
2492             int absDistanceX = Math.abs(distanceX);
2493             int absDistanceY = Math.abs(distanceY);
2494             mScrollStartY = mViewStartY;
2495             mPreviousDistanceX = 0;
2496             mPreviousDirection = 0;
2497
2498             // If the x distance is at least twice the y distance, then lock
2499             // the scroll horizontally.  Otherwise scroll vertically.
2500             if (absDistanceX >= 2 * absDistanceY) {
2501                 mTouchMode = TOUCH_MODE_HSCROLL;
2502                 mViewStartX = distanceX;
2503                 initNextView(-mViewStartX);
2504             } else {
2505                 mTouchMode = TOUCH_MODE_VSCROLL;
2506             }
2507         } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2508             // We are already scrolling horizontally, so check if we
2509             // changed the direction of scrolling so that the other week
2510             // is now visible.
2511             mViewStartX = distanceX;
2512             if (distanceX != 0) {
2513                 int direction = (distanceX > 0) ? 1 : -1;
2514                 if (direction != mPreviousDirection) {
2515                     // The user has switched the direction of scrolling
2516                     // so re-init the next view
2517                     initNextView(-mViewStartX);
2518                     mPreviousDirection = direction;
2519                 }
2520             }
2521
2522             // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
2523             // then change the title to the new day (or week), but only
2524             // if we haven't already changed the title.
2525             if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
2526                 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
2527                     CalendarView view = mParentActivity.getNextView();
2528                     mTitleTextView.setText(view.mDateRange);
2529                 }
2530             } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2531                 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
2532                     CalendarView view = mParentActivity.getNextView();
2533                     mTitleTextView.setText(view.mDateRange);
2534                 }
2535             } else {
2536                 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
2537                         || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2538                     mTitleTextView.setText(mDateRange);
2539                 }
2540             }
2541             mPreviousDistanceX = distanceX;
2542         }
2543
2544         if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
2545             mViewStartY = mScrollStartY + distanceY;
2546             if (mViewStartY < 0) {
2547                 mViewStartY = 0;
2548             } else if (mViewStartY > mMaxViewStartY) {
2549                 mViewStartY = mMaxViewStartY;
2550             }
2551             computeFirstHour();
2552         }
2553
2554         mScrolling = true;
2555
2556         if (mSelectionMode != SELECTION_HIDDEN) {
2557             mSelectionMode = SELECTION_HIDDEN;
2558             mRedrawScreen = true;
2559         }
2560         invalidate();
2561     }
2562
2563     void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2564         mTouchMode = TOUCH_MODE_INITIAL_STATE;
2565         mSelectionMode = SELECTION_HIDDEN;
2566         mOnFlingCalled = true;
2567         int deltaX = (int) e2.getX() - (int) e1.getX();
2568         int distanceX = Math.abs(deltaX);
2569         int deltaY = (int) e2.getY() - (int) e1.getY();
2570         int distanceY = Math.abs(deltaY);
2571
2572         if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
2573             boolean switchForward = initNextView(deltaX);
2574             CalendarView view = mParentActivity.getNextView();
2575             mTitleTextView.setText(view.mDateRange);
2576             mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
2577             mViewStartX = 0;
2578             return;
2579         }
2580
2581         // Continue scrolling vertically
2582         mContinueScroll.init((int) velocityY / 20);
2583         post(mContinueScroll);
2584     }
2585
2586     private boolean initNextView(int deltaX) {
2587         // Change the view to the previous day or week
2588         CalendarView view = mParentActivity.getNextView();
2589         Time date = view.mBaseDate;
2590         date.set(mBaseDate);
2591         boolean switchForward;
2592         if (deltaX > 0) {
2593             date.monthDay -= mNumDays;
2594             view.mSelectionDay = mSelectionDay - mNumDays;
2595             switchForward = false;
2596         } else {
2597             date.monthDay += mNumDays;
2598             view.mSelectionDay = mSelectionDay + mNumDays;
2599             switchForward = true;
2600         }
2601         date.normalize(true /* ignore isDst */);
2602         initView(view);
2603         view.layout(getLeft(), getTop(), getRight(), getBottom());
2604         view.reloadEvents();
2605         return switchForward;
2606     }
2607
2608     @Override
2609     public boolean onTouchEvent(MotionEvent ev) {
2610         int action = ev.getAction();
2611
2612         switch (action) {
2613         case MotionEvent.ACTION_DOWN:
2614             mParentActivity.mGestureDetector.onTouchEvent(ev);
2615             return true;
2616
2617         case MotionEvent.ACTION_MOVE:
2618             mParentActivity.mGestureDetector.onTouchEvent(ev);
2619             return true;
2620
2621         case MotionEvent.ACTION_UP:
2622             mParentActivity.mGestureDetector.onTouchEvent(ev);
2623             if (mOnFlingCalled) {
2624                 return true;
2625             }
2626             if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2627                 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2628                 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
2629                     // The user has gone beyond the threshold so switch views
2630                     mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
2631                     mViewStartX = 0;
2632                     return true;
2633                 } else {
2634                     // Not beyond the threshold so invalidate which will cause
2635                     // the view to snap back.  Also call recalc() to ensure
2636                     // that we have the correct starting date and title.
2637                     recalc();
2638                     mTitleTextView.setText(mDateRange);
2639                     invalidate();
2640                     mViewStartX = 0;
2641                 }
2642             }
2643
2644             // If we were scrolling, then reset the selected hour so that it
2645             // is visible.
2646             if (mScrolling) {
2647                 mScrolling = false;
2648                 resetSelectedHour();
2649                 mRedrawScreen = true;
2650                 invalidate();
2651             }
2652             return true;
2653
2654         // This case isn't expected to happen.
2655         case MotionEvent.ACTION_CANCEL:
2656             mParentActivity.mGestureDetector.onTouchEvent(ev);
2657             mScrolling = false;
2658             resetSelectedHour();
2659             return true;
2660
2661         default:
2662             if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
2663                 return true;
2664             }
2665             return super.onTouchEvent(ev);
2666         }
2667     }
2668
2669     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
2670         MenuItem item;
2671
2672         // If the trackball is held down, then the context menu pops up and
2673         // we never get onKeyUp() for the long-press.  So check for it here
2674         // and change the selection to the long-press state.
2675         if (mSelectionMode != SELECTION_LONGPRESS) {
2676             mSelectionMode = SELECTION_LONGPRESS;
2677             mRedrawScreen = true;
2678             invalidate();
2679         }
2680
2681         final long startMillis = getSelectedTimeInMillis();
2682         int flags = DateUtils.FORMAT_SHOW_TIME
2683                 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
2684                 | DateUtils.FORMAT_SHOW_WEEKDAY;
2685         final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
2686         menu.setHeaderTitle(title);
2687
2688         int numSelectedEvents = mSelectedEvents.size();
2689         if (mNumDays == 1) {
2690             // Day view.
2691
2692             // If there is a selected event, then allow it to be viewed and
2693             // edited.
2694             if (numSelectedEvents >= 1) {
2695                 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2696                 item.setOnMenuItemClickListener(mContextMenuHandler);
2697                 item.setIcon(android.R.drawable.ic_menu_info_details);
2698
2699                 if (isEventEditable(mParentActivity, mSelectedEvent)) {
2700                     item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2701                     item.setOnMenuItemClickListener(mContextMenuHandler);
2702                     item.setIcon(android.R.drawable.ic_menu_edit);
2703                     item.setAlphabeticShortcut('e');
2704
2705                     item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2706                     item.setOnMenuItemClickListener(mContextMenuHandler);
2707                     item.setIcon(android.R.drawable.ic_menu_delete);
2708                 }
2709
2710                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2711                 item.setOnMenuItemClickListener(mContextMenuHandler);
2712                 item.setIcon(android.R.drawable.ic_menu_add);
2713                 item.setAlphabeticShortcut('n');
2714             } else {
2715                 // Otherwise, if the user long-pressed on a blank hour, allow
2716                 // them to create an event.  They can also do this by tapping.
2717                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2718                 item.setOnMenuItemClickListener(mContextMenuHandler);
2719                 item.setIcon(android.R.drawable.ic_menu_add);
2720                 item.setAlphabeticShortcut('n');
2721             }
2722         } else {
2723             // Week view.
2724
2725             // If there is a selected event, then allow it to be viewed and
2726             // edited.
2727             if (numSelectedEvents >= 1) {
2728                 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2729                 item.setOnMenuItemClickListener(mContextMenuHandler);
2730                 item.setIcon(android.R.drawable.ic_menu_info_details);
2731
2732                 if (isEventEditable(mParentActivity, mSelectedEvent)) {
2733                     item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2734                     item.setOnMenuItemClickListener(mContextMenuHandler);
2735                     item.setIcon(android.R.drawable.ic_menu_edit);
2736                     item.setAlphabeticShortcut('e');
2737
2738                     item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2739                     item.setOnMenuItemClickListener(mContextMenuHandler);
2740                     item.setIcon(android.R.drawable.ic_menu_delete);
2741                 }
2742
2743                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2744                 item.setOnMenuItemClickListener(mContextMenuHandler);
2745                 item.setIcon(android.R.drawable.ic_menu_add);
2746                 item.setAlphabeticShortcut('n');
2747
2748                 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2749                 item.setOnMenuItemClickListener(mContextMenuHandler);
2750                 item.setIcon(android.R.drawable.ic_menu_day);
2751                 item.setAlphabeticShortcut('d');
2752
2753                 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2754                 item.setOnMenuItemClickListener(mContextMenuHandler);
2755                 item.setIcon(android.R.drawable.ic_menu_agenda);
2756                 item.setAlphabeticShortcut('a');
2757             } else {
2758                 // No events are selected
2759                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2760                 item.setOnMenuItemClickListener(mContextMenuHandler);
2761                 item.setIcon(android.R.drawable.ic_menu_add);
2762                 item.setAlphabeticShortcut('n');
2763
2764                 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2765                 item.setOnMenuItemClickListener(mContextMenuHandler);
2766                 item.setIcon(android.R.drawable.ic_menu_day);
2767                 item.setAlphabeticShortcut('d');
2768
2769                 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2770                 item.setOnMenuItemClickListener(mContextMenuHandler);
2771                 item.setIcon(android.R.drawable.ic_menu_agenda);
2772                 item.setAlphabeticShortcut('a');
2773             }
2774         }
2775
2776         mPopup.dismiss();
2777     }
2778
2779     private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
2780         public boolean onMenuItemClick(MenuItem item) {
2781             switch (item.getItemId()) {
2782                 case MenuHelper.MENU_EVENT_VIEW: {
2783                     if (mSelectedEvent != null) {
2784                         long id = mSelectedEvent.id;
2785                         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2786                         Intent intent = new Intent(Intent.ACTION_VIEW);
2787                         intent.setData(eventUri);
2788                         intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
2789                         intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2790                         intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2791                         mParentActivity.startActivity(intent);
2792                     }
2793                     break;
2794                 }
2795                 case MenuHelper.MENU_EVENT_EDIT: {
2796                     if (mSelectedEvent != null) {
2797                         long id = mSelectedEvent.id;
2798                         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2799                         Intent intent = new Intent(Intent.ACTION_EDIT);
2800                         intent.setData(eventUri);
2801                         intent.setClassName(mParentActivity, EditEvent.class.getName());
2802                         intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2803                         intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2804                         mParentActivity.startActivity(intent);
2805                     }
2806                     break;
2807                 }
2808                 case MenuHelper.MENU_DAY: {
2809                     long startMillis = getSelectedTimeInMillis();
2810                     Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
2811                     break;
2812                 }
2813                 case MenuHelper.MENU_AGENDA: {
2814                     long startMillis = getSelectedTimeInMillis();
2815                     Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
2816                     break;
2817                 }
2818                 case MenuHelper.MENU_EVENT_CREATE: {
2819                     long startMillis = getSelectedTimeInMillis();
2820                     long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
2821                     Intent intent = new Intent(Intent.ACTION_VIEW);
2822                     intent.setClassName(mParentActivity, EditEvent.class.getName());
2823                     intent.putExtra(EVENT_BEGIN_TIME, startMillis);
2824                     intent.putExtra(EVENT_END_TIME, endMillis);
2825                     intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
2826                     mParentActivity.startActivity(intent);
2827                     break;
2828                 }
2829                 case MenuHelper.MENU_EVENT_DELETE: {
2830                     if (mSelectedEvent != null) {
2831                         Event selectedEvent = mSelectedEvent;
2832                         long begin = selectedEvent.startMillis;
2833                         long end = selectedEvent.endMillis;
2834                         long id = selectedEvent.id;
2835                         mDeleteEventHelper.delete(begin, end, id, -1);
2836                     }
2837                     break;
2838                 }
2839                 default: {
2840                     return false;
2841                 }
2842             }
2843             return true;
2844         }
2845     }
2846
2847     private static boolean isEventEditable(Context context, Event e) {
2848         ContentResolver cr = context.getContentResolver();
2849
2850         int visibility = Calendars.NO_ACCESS;
2851         int relationship = Attendees.RELATIONSHIP_ORGANIZER;
2852
2853         // Get the calendar id for this event
2854         Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
2855                 new String[] { Events.CALENDAR_ID },
2856                 null /* selection */,
2857                 null /* selectionArgs */,
2858                 null /* sort */);
2859
2860         if (cursor == null) {
2861             return false;
2862         }
2863
2864         if (cursor.getCount() == 0) {
2865             cursor.close();
2866             return false;
2867         }
2868
2869         cursor.moveToFirst();
2870         long calId = cursor.getLong(0);
2871         cursor.close();
2872
2873         Uri uri = Calendars.CONTENT_URI;
2874         String where = String.format(CALENDARS_WHERE, calId);
2875         cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
2876
2877         String calendarOwnerAccount = null;
2878         if (cursor != null) {
2879             cursor.moveToFirst();
2880             visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
2881             calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
2882             cursor.close();
2883         }
2884
2885         if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
2886             return false;
2887         }
2888
2889         if (e.guestsCanModify) {
2890             return true;
2891         }
2892
2893         return !TextUtils.isEmpty(calendarOwnerAccount) && calendarOwnerAccount.equals(e.organizer);
2894     }
2895
2896     /**
2897      * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
2898      * If the touch position is not within the displayed grid, then this
2899      * method returns false.
2900      *
2901      * @param x the x position of the touch
2902      * @param y the y position of the touch
2903      * @return true if the touch position is valid
2904      */
2905     private boolean setSelectionFromPosition(int x, int y) {
2906         if (x < mHoursWidth) {
2907             return false;
2908         }
2909
2910         int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
2911         if (day >= mNumDays) {
2912             day = mNumDays - 1;
2913         }
2914         day += mFirstJulianDay;
2915         int hour;
2916         if (y < mFirstCell + mFirstHourOffset) {
2917             mSelectionAllDay = true;
2918         } else {
2919             hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
2920             hour += mFirstHour;
2921             mSelectionHour = hour;
2922             mSelectionAllDay = false;
2923         }
2924         mSelectionDay = day;
2925         findSelectedEvent(x, y);
2926 //        Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
2927 //                + " hour: " + hour
2928 //                + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
2929 //        if (mSelectedEvent != null) {
2930 //            Log.i("Cal", "  num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
2931 //            for (Event ev : mSelectedEvents) {
2932 //                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2933 //                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2934 //                String timeRange = formatDateRange(mParentActivity,
2935 //                        ev.startMillis, ev.endMillis, flags);
2936 //
2937 //                Log.i("Cal", "  " + timeRange + " " + ev.title);
2938 //            }
2939 //        }
2940         return true;
2941     }
2942
2943     private void findSelectedEvent(int x, int y) {
2944         int date = mSelectionDay;
2945         int cellWidth = mCellWidth;
2946         ArrayList<Event> events = mEvents;
2947         int numEvents = events.size();
2948         int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
2949         int top = 0;
2950         mSelectedEvent = null;
2951
2952         mSelectedEvents.clear();
2953         if (mSelectionAllDay) {
2954             float yDistance;
2955             float minYdistance = 10000.0f;  // any large number
2956             Event closestEvent = null;
2957             float drawHeight = mAllDayHeight;
2958             int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
2959             for (int i = 0; i < numEvents; i++) {
2960                 Event event = events.get(i);
2961                 if (!event.allDay) {
2962                     continue;
2963                 }
2964
2965                 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
2966                     float numRectangles = event.getMaxColumns();
2967                     float height = drawHeight / numRectangles;
2968                     if (height > MAX_ALLDAY_EVENT_HEIGHT) {
2969                         height = MAX_ALLDAY_EVENT_HEIGHT;
2970                     }
2971                     float eventTop = yOffset + height * event.getColumn();
2972                     float eventBottom = eventTop + height;
2973                     if (eventTop < y && eventBottom > y) {
2974                         // If the touch is inside the event rectangle, then
2975                         // add the event.
2976                         mSelectedEvents.add(event);
2977                         closestEvent = event;
2978                         break;
2979                     } else {
2980                         // Find the closest event
2981                         if (eventTop >= y) {
2982                             yDistance = eventTop - y;
2983                         } else {
2984                             yDistance = y - eventBottom;
2985                         }
2986                         if (yDistance < minYdistance) {
2987                             minYdistance = yDistance;
2988                             closestEvent = event;
2989                         }
2990                     }
2991                 }
2992             }
2993             mSelectedEvent = closestEvent;
2994             return;
2995         }
2996
2997         // Adjust y for the scrollable bitmap
2998         y += mViewStartY - mFirstCell;
2999
3000         // Use a region around (x,y) for the selection region
3001         Rect region = mRect;
3002         region.left = x - 10;
3003         region.right = x + 10;
3004         region.top = y - 10;
3005         region.bottom = y + 10;
3006
3007         EventGeometry geometry = mEventGeometry;
3008
3009         for (int i = 0; i < numEvents; i++) {
3010             Event event = events.get(i);
3011             // Compute the event rectangle.
3012             if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
3013                 continue;
3014             }
3015
3016             // If the event intersects the selection region, then add it to
3017             // mSelectedEvents.
3018             if (geometry.eventIntersectsSelection(event, region)) {
3019                 mSelectedEvents.add(event);
3020             }
3021         }
3022
3023         // If there are any events in the selected region, then assign the
3024         // closest one to mSelectedEvent.
3025         if (mSelectedEvents.size() > 0) {
3026             int len = mSelectedEvents.size();
3027             Event closestEvent = null;
3028             float minDist = mViewWidth + mViewHeight;  // some large distance
3029             for (int index = 0; index < len; index++) {
3030                 Event ev = mSelectedEvents.get(index);
3031                 float dist = geometry.pointToEvent(x, y, ev);
3032                 if (dist < minDist) {
3033                     minDist = dist;
3034                     closestEvent = ev;
3035                 }
3036             }
3037             mSelectedEvent = closestEvent;
3038
3039             // Keep the selected hour and day consistent with the selected
3040             // event.  They could be different if we touched on an empty hour
3041             // slot very close to an event in the previous hour slot.  In
3042             // that case we will select the nearby event.
3043             int startDay = mSelectedEvent.startDay;
3044             int endDay = mSelectedEvent.endDay;
3045             if (mSelectionDay < startDay) {
3046                 mSelectionDay = startDay;
3047             } else if (mSelectionDay > endDay) {
3048                 mSelectionDay = endDay;
3049             }
3050
3051             int startHour = mSelectedEvent.startTime / 60;
3052             int endHour;
3053             if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
3054                 endHour = (mSelectedEvent.endTime - 1) / 60;
3055             } else {
3056                 endHour = mSelectedEvent.endTime / 60;
3057             }
3058
3059             if (mSelectionHour < startHour) {
3060                 mSelectionHour = startHour;
3061             } else if (mSelectionHour > endHour) {
3062                 mSelectionHour = endHour;
3063             }
3064         }
3065     }
3066
3067     // Encapsulates the code to continue the scrolling after the
3068     // finger is lifted.  Instead of stopping the scroll immediately,
3069     // the scroll continues to "free spin" and gradually slows down.
3070     private class ContinueScroll implements Runnable {
3071         int mSignDeltaY;
3072         int mAbsDeltaY;
3073         float mFloatDeltaY;
3074         long mFreeSpinTime;
3075         private static final float FRICTION_COEF = 0.7F;
3076         private static final long FREE_SPIN_MILLIS = 180;
3077         private static final int MAX_DELTA = 60;
3078         private static final int SCROLL_REPEAT_INTERVAL = 30;
3079
3080         public void init(int deltaY) {
3081             mSignDeltaY = 0;
3082             if (deltaY > 0) {
3083                 mSignDeltaY = 1;
3084             } else if (deltaY < 0) {
3085                 mSignDeltaY = -1;
3086             }
3087             mAbsDeltaY = Math.abs(deltaY);
3088
3089             // Limit the maximum speed
3090             if (mAbsDeltaY > MAX_DELTA) {
3091                 mAbsDeltaY = MAX_DELTA;
3092             }
3093             mFloatDeltaY = mAbsDeltaY;
3094             mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
3095 //            Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
3096 //                    + " mViewStartY: " + mViewStartY);
3097         }
3098
3099         public void run() {
3100             long time = System.currentTimeMillis();
3101
3102             // Start out with a frictionless "free spin"
3103             if (time > mFreeSpinTime) {
3104                 // If the delta is small, then apply a fixed deceleration.
3105                 // Otherwise
3106                 if (mAbsDeltaY <= 10) {
3107                     mAbsDeltaY -= 2;
3108                 } else {
3109                     mFloatDeltaY *= FRICTION_COEF;
3110                     mAbsDeltaY = (int) mFloatDeltaY;
3111                 }
3112
3113                 if (mAbsDeltaY < 0) {
3114                     mAbsDeltaY = 0;
3115                 }
3116             }
3117
3118             if (mSignDeltaY == 1) {
3119                 mViewStartY -= mAbsDeltaY;
3120             } else {
3121                 mViewStartY += mAbsDeltaY;
3122             }
3123 //            Log.i("Cal", "  scroll: mAbsDeltaY: " + mAbsDeltaY
3124 //                    + " mViewStartY: " + mViewStartY);
3125
3126             if (mViewStartY < 0) {
3127                 mViewStartY = 0;
3128                 mAbsDeltaY = 0;
3129             } else if (mViewStartY > mMaxViewStartY) {
3130                 mViewStartY = mMaxViewStartY;
3131                 mAbsDeltaY = 0;
3132             }
3133
3134             computeFirstHour();
3135
3136             if (mAbsDeltaY > 0) {
3137                 postDelayed(this, SCROLL_REPEAT_INTERVAL);
3138             } else {
3139                 // Done scrolling.
3140                 mScrolling = false;
3141                 resetSelectedHour();
3142                 mRedrawScreen = true;
3143             }
3144
3145             invalidate();
3146         }
3147     }
3148
3149     /**
3150      * Cleanup the pop-up and timers.
3151      */
3152     public void cleanup() {
3153         // Protect against null-pointer exceptions
3154         if (mPopup != null) {
3155             mPopup.dismiss();
3156         }
3157         mLastPopupEventID = INVALID_EVENT_ID;
3158         Handler handler = getHandler();
3159         if (handler != null) {
3160             handler.removeCallbacks(mDismissPopup);
3161             handler.removeCallbacks(mUpdateCurrentTime);
3162         }
3163
3164         // Turn off redraw
3165         mRemeasure = false;
3166         mRedrawScreen = false;
3167     }
3168
3169     /**
3170      * Restart the update timer
3171      */
3172     public void restartCurrentTimeUpdates() {
3173         post(mUpdateCurrentTime);
3174     }
3175
3176     @Override protected void onDetachedFromWindow() {
3177         cleanup();
3178         if (mBitmap != null) {
3179             mBitmap.recycle();
3180             mBitmap = null;
3181         }
3182         super.onDetachedFromWindow();
3183     }
3184
3185     class DismissPopup implements Runnable {
3186         public void run() {
3187             // Protect against null-pointer exceptions
3188             if (mPopup != null) {
3189                 mPopup.dismiss();
3190             }
3191         }
3192     }
3193
3194     class UpdateCurrentTime implements Runnable {
3195         public void run() {
3196             long currentTime = System.currentTimeMillis();
3197             mCurrentTime.set(currentTime);
3198             //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
3199             postDelayed(mUpdateCurrentTime,
3200                     UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
3201             mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
3202             mRedrawScreen = true;
3203             invalidate();
3204         }
3205     }
3206 }
3207