OSDN Git Service

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