OSDN Git Service

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