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