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