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