DayView.java revision f7cf45455ce8cc5588479dcbb5d876f7638f79dc
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.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ObjectAnimator;
25import android.animation.TypeEvaluator;
26import android.animation.ValueAnimator;
27import android.app.AlertDialog;
28import android.app.Service;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.Context;
32import android.content.DialogInterface;
33import android.content.res.Resources;
34import android.content.res.TypedArray;
35import android.database.Cursor;
36import android.graphics.Canvas;
37import android.graphics.Paint;
38import android.graphics.Paint.Align;
39import android.graphics.Paint.Style;
40import android.graphics.Rect;
41import android.graphics.Typeface;
42import android.graphics.drawable.Drawable;
43import android.net.Uri;
44import android.os.Handler;
45import android.provider.CalendarContract.Attendees;
46import android.provider.CalendarContract.Calendars;
47import android.provider.CalendarContract.Events;
48import android.text.Layout.Alignment;
49import android.text.SpannableStringBuilder;
50import android.text.StaticLayout;
51import android.text.TextPaint;
52import android.text.TextUtils;
53import android.text.format.DateFormat;
54import android.text.format.DateUtils;
55import android.text.format.Time;
56import android.text.style.StyleSpan;
57import android.util.Log;
58import android.view.ContextMenu;
59import android.view.ContextMenu.ContextMenuInfo;
60import android.view.GestureDetector;
61import android.view.Gravity;
62import android.view.KeyEvent;
63import android.view.LayoutInflater;
64import android.view.MenuItem;
65import android.view.MotionEvent;
66import android.view.ScaleGestureDetector;
67import android.view.View;
68import android.view.ViewConfiguration;
69import android.view.ViewGroup;
70import android.view.WindowManager;
71import android.view.accessibility.AccessibilityEvent;
72import android.view.accessibility.AccessibilityManager;
73import android.view.animation.AccelerateDecelerateInterpolator;
74import android.view.animation.Animation;
75import android.view.animation.Interpolator;
76import android.view.animation.TranslateAnimation;
77import android.widget.EdgeEffect;
78import android.widget.ImageView;
79import android.widget.OverScroller;
80import android.widget.PopupWindow;
81import android.widget.TextView;
82import android.widget.ViewSwitcher;
83
84import java.util.ArrayList;
85import java.util.Arrays;
86import java.util.Calendar;
87import java.util.Formatter;
88import java.util.Locale;
89import java.util.regex.Matcher;
90import java.util.regex.Pattern;
91
92/**
93 * View for multi-day view. So far only 1 and 7 day have been tested.
94 */
95public class DayView extends View implements View.OnCreateContextMenuListener,
96        ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener
97        {
98    private static String TAG = "DayView";
99    private static boolean DEBUG = false;
100    private static boolean DEBUG_SCALING = false;
101    private static final String PERIOD_SPACE = ". ";
102
103    private static float mScale = 0; // Used for supporting different screen densities
104    private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
105    // Duration of the allday expansion
106    private static final long ANIMATION_DURATION = 400;
107    // duration of the more allday event text fade
108    private static final long ANIMATION_SECONDARY_DURATION = 200;
109
110    private static final int MENU_AGENDA = 2;
111    private static final int MENU_DAY = 3;
112    private static final int MENU_EVENT_VIEW = 5;
113    private static final int MENU_EVENT_CREATE = 6;
114    private static final int MENU_EVENT_EDIT = 7;
115    private static final int MENU_EVENT_DELETE = 8;
116
117    private static int DEFAULT_CELL_HEIGHT = 64;
118    private static int MAX_CELL_HEIGHT = 150;
119    private static int MIN_Y_SPAN = 100;
120
121    private boolean mOnFlingCalled;
122    private boolean mStartingScroll = false;
123    /**
124     * ID of the last event which was displayed with the toast popup.
125     *
126     * This is used to prevent popping up multiple quick views for the same event, especially
127     * during calendar syncs. This becomes valid when an event is selected, either by default
128     * on starting calendar or by scrolling to an event. It becomes invalid when the user
129     * explicitly scrolls to an empty time slot, changes views, or deletes the event.
130     */
131    private long mLastPopupEventID;
132
133    protected Context mContext;
134
135    private static final String[] CALENDARS_PROJECTION = new String[] {
136        Calendars._ID,          // 0
137        Calendars.CALENDAR_ACCESS_LEVEL, // 1
138        Calendars.OWNER_ACCOUNT, // 2
139    };
140    private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
141    private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
142    private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
143
144    private static final int FROM_NONE = 0;
145    private static final int FROM_ABOVE = 1;
146    private static final int FROM_BELOW = 2;
147    private static final int FROM_LEFT = 4;
148    private static final int FROM_RIGHT = 8;
149
150    private static final int ACCESS_LEVEL_NONE = 0;
151    private static final int ACCESS_LEVEL_DELETE = 1;
152    private static final int ACCESS_LEVEL_EDIT = 2;
153
154    private static int mHorizontalSnapBackThreshold = 128;
155
156    private ContinueScroll mContinueScroll = new ContinueScroll();
157
158    // Make this visible within the package for more informative debugging
159    Time mBaseDate;
160    private Time mCurrentTime;
161    //Update the current time line every five minutes if the window is left open that long
162    private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
163    private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
164    private int mTodayJulianDay;
165
166    private Typeface mBold = Typeface.DEFAULT_BOLD;
167    private int mFirstJulianDay;
168    private int mLastJulianDay;
169
170    private int mMonthLength;
171    private int mFirstVisibleDate;
172    private int mFirstVisibleDayOfWeek;
173    private int[] mEarliestStartHour;    // indexed by the week day offset
174    private boolean[] mHasAllDayEvent;   // indexed by the week day offset
175    private String mEventCountTemplate;
176    private CharSequence[] mLongPressItems;
177    private String mLongPressTitle;
178
179    protected static StringBuilder mStringBuilder = new StringBuilder(50);
180    // TODO recreate formatter when locale changes
181    protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
182
183    private Runnable mTZUpdater = new Runnable() {
184        @Override
185        public void run() {
186            String tz = Utils.getTimeZone(mContext, this);
187            mBaseDate.timezone = tz;
188            mBaseDate.normalize(true);
189            mCurrentTime.switchTimezone(tz);
190            invalidate();
191        }
192    };
193
194    AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
195        @Override
196        public void onAnimationStart(Animator animation) {
197            mScrolling = true;
198        }
199
200        @Override
201        public void onAnimationCancel(Animator animation) {
202            mScrolling = false;
203        }
204
205        @Override
206        public void onAnimationEnd(Animator animation) {
207            mScrolling = false;
208            resetSelectedHour();
209            invalidate();
210        }
211    };
212
213    /**
214     * This variable helps to avoid unnecessarily reloading events by keeping
215     * track of the start millis parameter used for the most recent loading
216     * of events.  If the next reload matches this, then the events are not
217     * reloaded.  To force a reload, set this to zero (this is set to zero
218     * in the method clearCachedEvents()).
219     */
220    private long mLastReloadMillis;
221
222    private ArrayList<Event> mEvents = new ArrayList<Event>();
223    private ArrayList<Event> mAllDayEvents = new ArrayList<Event>();
224    private StaticLayout[] mLayouts = null;
225    private StaticLayout[] mAllDayLayouts = null;
226    private int mSelectionDay;        // Julian day
227    private int mSelectionHour;
228
229    boolean mSelectionAllday;
230
231    private int mLastSelectionDay;
232    private int mLastSelectionHour;
233    private Event mLastSelectedEvent;
234
235    /** Width of a day or non-conflicting event */
236    private int mCellWidth;
237
238    // Pre-allocate these objects and re-use them
239    private Rect mRect = new Rect();
240    private Rect mDestRect = new Rect();
241    private Rect mSelectionRect = new Rect();
242    // This encloses the more allDay events icon
243    private Rect mExpandAllDayRect = new Rect();
244    // TODO Clean up paint usage
245    private Paint mPaint = new Paint();
246    private Paint mEventTextPaint = new Paint();
247    private Paint mSelectionPaint = new Paint();
248    private float[] mLines;
249
250    private int mFirstDayOfWeek; // First day of the week
251
252    private PopupWindow mPopup;
253    private View mPopupView;
254
255    // The number of milliseconds to show the popup window
256    private static final int POPUP_DISMISS_DELAY = 3000;
257    private DismissPopup mDismissPopup = new DismissPopup();
258
259    private boolean mRemeasure = true;
260
261    private final EventLoader mEventLoader;
262    protected final EventGeometry mEventGeometry;
263
264    private static float GRID_LINE_LEFT_MARGIN = 0;
265    private static final float GRID_LINE_INNER_WIDTH = 1;
266
267    private static final int DAY_GAP = 1;
268    private static final int HOUR_GAP = 1;
269    // This is the standard height of an allday event with no restrictions
270    private static int SINGLE_ALLDAY_HEIGHT = 34;
271    /**
272    * This is the minimum desired height of a allday event.
273    * When unexpanded, allday events will use this height.
274    * When expanded allDay events will attempt to grow to fit all
275    * events at this height.
276    */
277    private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels
278    /**
279     * This is how big the unexpanded allday height is allowed to be.
280     * It will get adjusted based on screen size
281     */
282    private static int MAX_UNEXPANDED_ALLDAY_HEIGHT =
283            (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4);
284    /**
285     * This is the minimum size reserved for displaying regular events.
286     * The expanded allDay region can't expand into this.
287     */
288    private static int MIN_HOURS_HEIGHT = 180;
289    private static int ALLDAY_TOP_MARGIN = 1;
290    // The largest a single allDay event will become.
291    private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34;
292
293    private static int HOURS_TOP_MARGIN = 2;
294    private static int HOURS_LEFT_MARGIN = 2;
295    private static int HOURS_RIGHT_MARGIN = 4;
296    private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
297    private static int NEW_EVENT_MARGIN = 4;
298    private static int NEW_EVENT_WIDTH = 5;
299    private static int NEW_EVENT_MAX_LENGTH = 16;
300
301    private static int CURRENT_TIME_LINE_HEIGHT = 2;
302    private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
303    private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4;
304    private static int CURRENT_TIME_LINE_TOP_OFFSET = 2;
305
306    /* package */ static final int MINUTES_PER_HOUR = 60;
307    /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
308    /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
309    /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
310    /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
311
312    private static final int DECLINED_ALPHA = 0x66000000;
313    // More events text will transition between invisible and this alpha
314    private static final int MORE_EVENTS_MAX_ALPHA = 0x4C;
315    private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0;
316    private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5;
317    private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6;
318    private static int DAY_HEADER_LEFT_MARGIN = 5;
319    private static int DAY_HEADER_RIGHT_MARGIN = 4;
320    private static int DAY_HEADER_BOTTOM_MARGIN = 3;
321    private static float DAY_HEADER_FONT_SIZE = 14;
322    private static float DATE_HEADER_FONT_SIZE = 32;
323    private static float NORMAL_FONT_SIZE = 12;
324    private static float EVENT_TEXT_FONT_SIZE = 12;
325    private static float HOURS_TEXT_SIZE = 12;
326    private static float AMPM_TEXT_SIZE = 9;
327    private static int MIN_HOURS_WIDTH = 96;
328    private static int MIN_CELL_WIDTH_FOR_TEXT = 20;
329    private static final int MAX_EVENT_TEXT_LEN = 500;
330    // smallest height to draw an event with
331    private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels
332    private static int CALENDAR_COLOR_SQUARE_SIZE = 10;
333    private static int EVENT_RECT_TOP_MARGIN = 1;
334    private static int EVENT_RECT_BOTTOM_MARGIN = 0;
335    private static int EVENT_RECT_LEFT_MARGIN = 1;
336    private static int EVENT_RECT_RIGHT_MARGIN = 0;
337    private static int EVENT_RECT_STROKE_WIDTH = 2;
338    private static int EVENT_TEXT_TOP_MARGIN = 2;
339    private static int EVENT_TEXT_BOTTOM_MARGIN = 2;
340    private static int EVENT_TEXT_LEFT_MARGIN = 6;
341    private static int EVENT_TEXT_RIGHT_MARGIN = 6;
342    private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1;
343    private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN;
344    private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN;
345    private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN;
346    private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN;
347    // margins and sizing for the expand allday icon
348    private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10;
349    // sizing for "box +n" in allDay events
350    private static int EVENT_SQUARE_WIDTH = 10;
351    private static int EVENT_LINE_PADDING = 4;
352
353    private static int mPressedColor;
354    private static int mEventTextColor;
355    private static int mMoreEventsTextColor;
356
357    private static int mWeek_saturdayColor;
358    private static int mWeek_sundayColor;
359    private static int mCalendarDateBannerTextColor;
360    private static int mCalendarAmPmLabel;
361    private static int mCalendarGridAreaSelected;
362    private static int mCalendarGridLineInnerHorizontalColor;
363    private static int mCalendarGridLineInnerVerticalColor;
364    private static int mFutureBgColor;
365    private static int mBgColor;
366    private static int mCalendarHourLabelColor;
367    private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA;
368
369    private float mAnimationDistance = 0;
370    private int mViewStartX;
371    private int mViewStartY;
372    private int mMaxViewStartY;
373    private int mViewHeight;
374    private int mViewWidth;
375    private int mGridAreaHeight = -1;
376    private static int mCellHeight = 0; // shared among all DayViews
377    private static int mMinCellHeight = 32;
378    private int mScrollStartY;
379    private int mPreviousDirection;
380
381    /**
382     * Vertical distance or span between the two touch points at the start of a
383     * scaling gesture
384     */
385    private float mStartingSpanY = 0;
386    /** Height of 1 hour in pixels at the start of a scaling gesture */
387    private int mCellHeightBeforeScaleGesture;
388    /** The hour at the center two touch points */
389    private float mGestureCenterHour = 0;
390    /**
391     * Flag to decide whether to handle the up event. Cases where up events
392     * should be ignored are 1) right after a scale gesture and 2) finger was
393     * down before app launch
394     */
395    private boolean mHandleActionUp = true;
396
397    private int mHoursTextHeight;
398    /**
399     * The height of the area used for allday events
400     */
401    private int mAlldayHeight;
402    /**
403     * The height of the allday event area used during animation
404     */
405    private int mAnimateDayHeight = 0;
406    /**
407     * The height of an individual allday event during animation
408     */
409    private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
410    /**
411     * Whether to use the expand or collapse icon.
412     */
413    private static boolean mUseExpandIcon = true;
414    /**
415     * The height of the day names/numbers
416     */
417    private static int DAY_HEADER_HEIGHT = 45;
418    /**
419     * The height of the day names/numbers for multi-day views
420     */
421    private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT;
422    /**
423     * The height of the day names/numbers when viewing a single day
424     */
425    private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT;
426    /**
427     * Max of all day events in a given day in this view.
428     */
429    private int mMaxAlldayEvents;
430    /**
431     * A count of the number of allday events that were not drawn for each day
432     */
433    private int[] mSkippedAlldayEvents;
434    /**
435     * The number of allDay events at which point we start hiding allDay events.
436     */
437    private int mMaxUnexpandedAlldayEventCount = 4;
438    /**
439     * Whether or not to expand the allDay area to fill the screen
440     */
441    private static boolean mShowAllAllDayEvents = false;
442
443    protected int mNumDays = 7;
444    private int mNumHours = 10;
445
446    /** Width of the time line (list of hours) to the left. */
447    private int mHoursWidth;
448    private int mDateStrWidth;
449    /** Top of the scrollable region i.e. below date labels and all day events */
450    private int mFirstCell;
451    /** First fully visibile hour */
452    private int mFirstHour = -1;
453    /** Distance between the mFirstCell and the top of first fully visible hour. */
454    private int mFirstHourOffset;
455    private String[] mHourStrs;
456    private String[] mDayStrs;
457    private String[] mDayStrs2Letter;
458    private boolean mIs24HourFormat;
459
460    private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
461    private boolean mComputeSelectedEvents;
462    private boolean mUpdateToast;
463    private Event mSelectedEvent;
464    private Event mPrevSelectedEvent;
465    private Rect mPrevBox = new Rect();
466    protected final Resources mResources;
467    protected final Drawable mCurrentTimeLine;
468    protected final Drawable mTodayHeaderDrawable;
469    protected final Drawable mExpandAlldayDrawable;
470    protected final Drawable mCollapseAlldayDrawable;
471    protected Drawable mAcceptedOrTentativeEventBoxDrawable;
472    private String mAmString;
473    private String mPmString;
474    private DeleteEventHelper mDeleteEventHelper;
475    private static int sCounter = 0;
476
477    private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
478
479    ScaleGestureDetector mScaleGestureDetector;
480
481    /**
482     * The initial state of the touch mode when we enter this view.
483     */
484    private static final int TOUCH_MODE_INITIAL_STATE = 0;
485
486    /**
487     * Indicates we just received the touch event and we are waiting to see if
488     * it is a tap or a scroll gesture.
489     */
490    private static final int TOUCH_MODE_DOWN = 1;
491
492    /**
493     * Indicates the touch gesture is a vertical scroll
494     */
495    private static final int TOUCH_MODE_VSCROLL = 0x20;
496
497    /**
498     * Indicates the touch gesture is a horizontal scroll
499     */
500    private static final int TOUCH_MODE_HSCROLL = 0x40;
501
502    private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
503
504    /**
505     * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
506     */
507    private static final int SELECTION_HIDDEN = 0;
508    private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet
509    private static final int SELECTION_SELECTED = 2;
510    private static final int SELECTION_LONGPRESS = 3;
511
512    private int mSelectionMode = SELECTION_HIDDEN;
513
514    private boolean mScrolling = false;
515
516    private float mInitialScrollX;
517    private float mInitialScrollY;
518
519    // Animates the height of the allday region
520    ObjectAnimator mAlldayAnimator;
521    // Animates the height of events in the allday region
522    ObjectAnimator mAlldayEventAnimator;
523    // Animates the transparency of the more events text
524    ObjectAnimator mMoreAlldayEventsAnimator;
525    // whether or not an event is stopping because it was cancelled
526    private boolean mCancellingAnimations = false;
527    // tracks whether a touch originated in the allday area
528    private boolean mTouchStartedInAlldayArea = false;
529
530    private CalendarController mController;
531    private ViewSwitcher mViewSwitcher;
532    private GestureDetector mGestureDetector;
533    private OverScroller mScroller;
534    private EdgeEffect mEdgeEffectTop;
535    private EdgeEffect mEdgeEffectBottom;
536    private boolean mCallEdgeEffectOnAbsorb;
537    private final int OVERFLING_DISTANCE;
538    private float mLastVelocity;
539
540    private ScrollInterpolator mHScrollInterpolator;
541    private AccessibilityManager mAccessibilityMgr = null;
542    private boolean mIsAccessibilityEnabled = false;
543    private boolean mTouchExplorationEnabled = false;
544    private String mCreateNewEventString;
545
546    public DayView(Context context, CalendarController controller,
547            ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) {
548        super(context);
549        mContext = context;
550        initAccessibilityVariables();
551
552        mResources = context.getResources();
553        mCreateNewEventString = mResources.getString(R.string.event_create);
554
555        DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size);
556        DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size);
557        ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height);
558        DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin);
559        EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin);
560        HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size);
561        AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size);
562        MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width);
563        HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin);
564        HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin);
565        MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height);
566        EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(R.dimen.event_text_size);
567        MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height);
568        MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT;
569        EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin);
570        EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN;
571        EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN;
572        EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN;
573
574        EVENT_TEXT_LEFT_MARGIN = (int) mResources
575                .getDimension(R.dimen.event_text_horizontal_margin);
576        EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN;
577        EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN;
578        EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN;
579
580        if (mScale == 0) {
581
582            mScale = mResources.getDisplayMetrics().density;
583            if (mScale != 1) {
584                SINGLE_ALLDAY_HEIGHT *= mScale;
585                ALLDAY_TOP_MARGIN *= mScale;
586                MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale;
587
588                NORMAL_FONT_SIZE *= mScale;
589                GRID_LINE_LEFT_MARGIN *= mScale;
590                HOURS_TOP_MARGIN *= mScale;
591                MIN_CELL_WIDTH_FOR_TEXT *= mScale;
592                MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale;
593                mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
594
595                CURRENT_TIME_LINE_HEIGHT *= mScale;
596                CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
597                CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;
598                CURRENT_TIME_LINE_TOP_OFFSET *= mScale;
599
600                MIN_Y_SPAN *= mScale;
601                MAX_CELL_HEIGHT *= mScale;
602                DEFAULT_CELL_HEIGHT *= mScale;
603                DAY_HEADER_HEIGHT *= mScale;
604                DAY_HEADER_LEFT_MARGIN *= mScale;
605                DAY_HEADER_RIGHT_MARGIN *= mScale;
606                DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale;
607                DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale;
608                DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale;
609                CALENDAR_COLOR_SQUARE_SIZE *= mScale;
610                EVENT_RECT_TOP_MARGIN *= mScale;
611                EVENT_RECT_BOTTOM_MARGIN *= mScale;
612                ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale;
613                EVENT_RECT_LEFT_MARGIN *= mScale;
614                EVENT_RECT_RIGHT_MARGIN *= mScale;
615                EVENT_RECT_STROKE_WIDTH *= mScale;
616                EVENT_SQUARE_WIDTH *= mScale;
617                EVENT_LINE_PADDING *= mScale;
618                NEW_EVENT_MARGIN *= mScale;
619                NEW_EVENT_WIDTH *= mScale;
620                NEW_EVENT_MAX_LENGTH *= mScale;
621            }
622        }
623        HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
624        DAY_HEADER_HEIGHT = numDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT;
625
626        mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light);
627        mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light);
628        mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_expand_holo_light);
629        mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_collapse_holo_light);
630        mAcceptedOrTentativeEventBoxDrawable = mResources
631                .getDrawable(R.drawable.panel_month_event_holo_light);
632
633        mEventLoader = eventLoader;
634        mEventGeometry = new EventGeometry();
635        mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
636        mEventGeometry.setHourGap(HOUR_GAP);
637        mEventGeometry.setCellMargin(DAY_GAP);
638        mLongPressItems = new CharSequence[] {
639            mResources.getString(R.string.new_event_dialog_option)
640        };
641        mLongPressTitle = mResources.getString(R.string.new_event_dialog_label);
642        mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */);
643        mLastPopupEventID = INVALID_EVENT_ID;
644        mController = controller;
645        mViewSwitcher = viewSwitcher;
646        mGestureDetector = new GestureDetector(context, new CalendarGestureListener());
647        mScaleGestureDetector = new ScaleGestureDetector(getContext(), this);
648        mNumDays = numDays;
649        if (mCellHeight == 0) {
650            mCellHeight = Utils.getSharedPreference(mContext,
651                    GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT);
652        }
653        mScroller = new OverScroller(context);
654        mHScrollInterpolator = new ScrollInterpolator();
655        mEdgeEffectTop = new EdgeEffect(context);
656        mEdgeEffectBottom = new EdgeEffect(context);
657        OVERFLING_DISTANCE = ViewConfiguration.get(context).getScaledOverflingDistance();
658
659        init(context);
660    }
661
662    private void init(Context context) {
663        setFocusable(true);
664
665        // Allow focus in touch mode so that we can do keyboard shortcuts
666        // even after we've entered touch mode.
667        setFocusableInTouchMode(true);
668        setClickable(true);
669        setOnCreateContextMenuListener(this);
670
671        mFirstDayOfWeek = Utils.getFirstDayOfWeek(context);
672
673        mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater));
674        long currentTime = System.currentTimeMillis();
675        mCurrentTime.set(currentTime);
676        //The % makes it go off at the next increment of 5 minutes.
677        postDelayed(mUpdateCurrentTime,
678                UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
679        mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
680
681        mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
682        mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
683        mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
684        mFutureBgColor = mResources.getColor(R.color.calendar_future_bg_color);
685        mBgColor = mResources.getColor(R.color.calendar_hour_background);
686        mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
687        mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
688        mCalendarGridLineInnerHorizontalColor = mResources
689                .getColor(R.color.calendar_grid_line_inner_horizontal_color);
690        mCalendarGridLineInnerVerticalColor = mResources
691                .getColor(R.color.calendar_grid_line_inner_vertical_color);
692        mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label);
693        mPressedColor = mResources.getColor(R.color.pressed);
694        mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
695        mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color);
696
697        mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
698        mEventTextPaint.setTextAlign(Paint.Align.LEFT);
699        mEventTextPaint.setAntiAlias(true);
700
701        int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
702        Paint p = mSelectionPaint;
703        p.setColor(gridLineColor);
704        p.setStyle(Style.FILL);
705        p.setAntiAlias(false);
706
707        p = mPaint;
708        p.setAntiAlias(true);
709
710        // Allocate space for 2 weeks worth of weekday names so that we can
711        // easily start the week display at any week day.
712        mDayStrs = new String[14];
713
714        // Also create an array of 2-letter abbreviations.
715        mDayStrs2Letter = new String[14];
716
717        for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
718            int index = i - Calendar.SUNDAY;
719            // e.g. Tue for Tuesday
720            mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM)
721                    .toUpperCase();
722            mDayStrs[index + 7] = mDayStrs[index];
723            // e.g. Tu for Tuesday
724            mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT)
725                    .toUpperCase();
726
727            // If we don't have 2-letter day strings, fall back to 1-letter.
728            if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
729                mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
730            }
731
732            mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
733        }
734
735        // Figure out how much space we need for the 3-letter abbrev names
736        // in the worst case.
737        p.setTextSize(DATE_HEADER_FONT_SIZE);
738        p.setTypeface(mBold);
739        String[] dateStrs = {" 28", " 30"};
740        mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
741        p.setTextSize(DAY_HEADER_FONT_SIZE);
742        mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
743
744        p.setTextSize(HOURS_TEXT_SIZE);
745        p.setTypeface(null);
746        handleOnResume();
747
748        mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase();
749        mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase();
750        String[] ampm = {mAmString, mPmString};
751        p.setTextSize(AMPM_TEXT_SIZE);
752        mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p)
753                + HOURS_RIGHT_MARGIN);
754        mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth);
755
756        LayoutInflater inflater;
757        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
758        mPopupView = inflater.inflate(R.layout.bubble_event, null);
759        mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
760                ViewGroup.LayoutParams.MATCH_PARENT,
761                ViewGroup.LayoutParams.WRAP_CONTENT));
762        mPopup = new PopupWindow(context);
763        mPopup.setContentView(mPopupView);
764        Resources.Theme dialogTheme = getResources().newTheme();
765        dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
766        TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
767            android.R.attr.windowBackground });
768        mPopup.setBackgroundDrawable(ta.getDrawable(0));
769        ta.recycle();
770
771        // Enable touching the popup window
772        mPopupView.setOnClickListener(this);
773        // Catch long clicks for creating a new event
774        setOnLongClickListener(this);
775
776        mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater));
777        long millis = System.currentTimeMillis();
778        mBaseDate.set(millis);
779
780        mEarliestStartHour = new int[mNumDays];
781        mHasAllDayEvent = new boolean[mNumDays];
782
783        // mLines is the array of points used with Canvas.drawLines() in
784        // drawGridBackground() and drawAllDayEvents().  Its size depends
785        // on the max number of lines that can ever be drawn by any single
786        // drawLines() call in either of those methods.
787        final int maxGridLines = (24 + 1)  // max horizontal lines we might draw
788                + (mNumDays + 1); // max vertical lines we might draw
789        mLines = new float[maxGridLines * 4];
790    }
791
792    /**
793     * This is called when the popup window is pressed.
794     */
795    public void onClick(View v) {
796        if (v == mPopupView) {
797            // Pretend it was a trackball click because that will always
798            // jump to the "View event" screen.
799            switchViews(true /* trackball */);
800        }
801    }
802
803    public void handleOnResume() {
804        initAccessibilityVariables();
805        mIs24HourFormat = DateFormat.is24HourFormat(mContext);
806        mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
807        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
808        mLastSelectionDay = 0;
809        mLastSelectionHour = 0;
810        mLastSelectedEvent = null;
811        mSelectionMode = SELECTION_HIDDEN;
812    }
813
814    private void initAccessibilityVariables() {
815        mAccessibilityMgr = (AccessibilityManager) mContext
816                .getSystemService(Service.ACCESSIBILITY_SERVICE);
817        mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr.isEnabled();
818        mTouchExplorationEnabled = isTouchExplorationEnabled();
819    }
820
821    /**
822     * Returns the start of the selected time in milliseconds since the epoch.
823     *
824     * @return selected time in UTC milliseconds since the epoch.
825     */
826    long getSelectedTimeInMillis() {
827        Time time = new Time(mBaseDate);
828        time.setJulianDay(mSelectionDay);
829        time.hour = mSelectionHour;
830
831        // We ignore the "isDst" field because we want normalize() to figure
832        // out the correct DST value and not adjust the selected time based
833        // on the current setting of DST.
834        return time.normalize(true /* ignore isDst */);
835    }
836
837    Time getSelectedTime() {
838        Time time = new Time(mBaseDate);
839        time.setJulianDay(mSelectionDay);
840        time.hour = mSelectionHour;
841
842        // We ignore the "isDst" field because we want normalize() to figure
843        // out the correct DST value and not adjust the selected time based
844        // on the current setting of DST.
845        time.normalize(true /* ignore isDst */);
846        return time;
847    }
848
849    /**
850     * Returns the start of the selected time in minutes since midnight,
851     * local time.  The derived class must ensure that this is consistent
852     * with the return value from getSelectedTimeInMillis().
853     */
854    int getSelectedMinutesSinceMidnight() {
855        return mSelectionHour * MINUTES_PER_HOUR;
856    }
857
858    int getFirstVisibleHour() {
859        return mFirstHour;
860    }
861
862    void setFirstVisibleHour(int firstHour) {
863        mFirstHour = firstHour;
864        mFirstHourOffset = 0;
865    }
866
867    public void setSelected(Time time, boolean ignoreTime) {
868        mBaseDate.set(time);
869        mSelectionHour = mBaseDate.hour;
870        mSelectedEvent = null;
871        mPrevSelectedEvent = null;
872        long millis = mBaseDate.toMillis(false /* use isDst */);
873        mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
874        mSelectedEvents.clear();
875        mComputeSelectedEvents = true;
876
877        int gotoY = Integer.MIN_VALUE;
878
879        if (!ignoreTime && mGridAreaHeight != -1) {
880            int lastHour = 0;
881
882            if (mBaseDate.hour < mFirstHour) {
883                // Above visible region
884                gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP);
885            } else {
886                lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP)
887                        + mFirstHour;
888
889                if (mBaseDate.hour >= lastHour) {
890                    // Below visible region
891
892                    // target hour + 1 (to give it room to see the event) -
893                    // grid height (to get the y of the top of the visible
894                    // region)
895                    gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f)
896                            * (mCellHeight + HOUR_GAP) - mGridAreaHeight);
897                }
898            }
899
900            if (DEBUG) {
901                Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH "
902                        + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight
903                        + " ymax " + mMaxViewStartY);
904            }
905
906            if (gotoY > mMaxViewStartY) {
907                gotoY = mMaxViewStartY;
908            } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) {
909                gotoY = 0;
910            }
911        }
912
913        recalc();
914
915        mRemeasure = true;
916        invalidate();
917
918        if (gotoY != Integer.MIN_VALUE) {
919            TypeEvaluator<Object> evaluator = new TypeEvaluator<Object>() {
920                @Override
921                public Object evaluate(float fraction, Object startValue, Object endValue) {
922                    int start = (Integer) startValue;
923                    int end = (Integer) endValue;
924                    final int newValue = (int) ((end - start) * fraction + start);
925                    setViewStartY(newValue);
926                    return new Integer(newValue);
927                }
928            };
929            ValueAnimator scrollAnim = ObjectAnimator.ofObject(evaluator, new Integer(mViewStartY),
930                    new Integer(gotoY));
931//          TODO The following line is supposed to replace the two statements above.
932//          Need to investigate why it's not working.
933
934//          ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY);
935            scrollAnim.setDuration(200);
936            scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator());
937            scrollAnim.addListener(mAnimatorListener);
938            scrollAnim.start();
939        }
940        sendAccessibilityEventAsNeeded(false);
941    }
942
943    public void setViewStartY(int viewStartY) {
944        if (viewStartY > mMaxViewStartY) {
945            viewStartY = mMaxViewStartY;
946        }
947
948        mViewStartY = viewStartY;
949
950        computeFirstHour();
951        invalidate();
952    }
953
954    public Time getSelectedDay() {
955        Time time = new Time(mBaseDate);
956        time.setJulianDay(mSelectionDay);
957        time.hour = mSelectionHour;
958
959        // We ignore the "isDst" field because we want normalize() to figure
960        // out the correct DST value and not adjust the selected time based
961        // on the current setting of DST.
962        time.normalize(true /* ignore isDst */);
963        return time;
964    }
965
966    public void updateTitle() {
967        Time start = new Time(mBaseDate);
968        start.normalize(true);
969        Time end = new Time(start);
970        end.monthDay += mNumDays - 1;
971        // Move it forward one minute so the formatter doesn't lose a day
972        end.minute += 1;
973        end.normalize(true);
974
975        long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
976        if (mNumDays != 1) {
977            // Don't show day of the month if for multi-day view
978            formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY;
979
980            // Abbreviate the month if showing multiple months
981            if (start.month != end.month) {
982                formatFlags |= DateUtils.FORMAT_ABBREV_MONTH;
983            }
984        }
985
986        mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT,
987                formatFlags, null, null);
988    }
989
990    /**
991     * return a negative number if "time" is comes before the visible time
992     * range, a positive number if "time" is after the visible time range, and 0
993     * if it is in the visible time range.
994     */
995    public int compareToVisibleTimeRange(Time time) {
996
997        int savedHour = mBaseDate.hour;
998        int savedMinute = mBaseDate.minute;
999        int savedSec = mBaseDate.second;
1000
1001        mBaseDate.hour = 0;
1002        mBaseDate.minute = 0;
1003        mBaseDate.second = 0;
1004
1005        if (DEBUG) {
1006            Log.d(TAG, "Begin " + mBaseDate.toString());
1007            Log.d(TAG, "Diff  " + time.toString());
1008        }
1009
1010        // Compare beginning of range
1011        int diff = Time.compare(time, mBaseDate);
1012        if (diff > 0) {
1013            // Compare end of range
1014            mBaseDate.monthDay += mNumDays;
1015            mBaseDate.normalize(true);
1016            diff = Time.compare(time, mBaseDate);
1017
1018            if (DEBUG) Log.d(TAG, "End   " + mBaseDate.toString());
1019
1020            mBaseDate.monthDay -= mNumDays;
1021            mBaseDate.normalize(true);
1022            if (diff < 0) {
1023                // in visible time
1024                diff = 0;
1025            } else if (diff == 0) {
1026                // Midnight of following day
1027                diff = 1;
1028            }
1029        }
1030
1031        if (DEBUG) Log.d(TAG, "Diff: " + diff);
1032
1033        mBaseDate.hour = savedHour;
1034        mBaseDate.minute = savedMinute;
1035        mBaseDate.second = savedSec;
1036        return diff;
1037    }
1038
1039    private void recalc() {
1040        // Set the base date to the beginning of the week if we are displaying
1041        // 7 days at a time.
1042        if (mNumDays == 7) {
1043            adjustToBeginningOfWeek(mBaseDate);
1044        }
1045
1046        final long start = mBaseDate.toMillis(false /* use isDst */);
1047        mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
1048        mLastJulianDay = mFirstJulianDay + mNumDays - 1;
1049
1050        mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
1051        mFirstVisibleDate = mBaseDate.monthDay;
1052        mFirstVisibleDayOfWeek = mBaseDate.weekDay;
1053    }
1054
1055    private void adjustToBeginningOfWeek(Time time) {
1056        int dayOfWeek = time.weekDay;
1057        int diff = dayOfWeek - mFirstDayOfWeek;
1058        if (diff != 0) {
1059            if (diff < 0) {
1060                diff += 7;
1061            }
1062            time.monthDay -= diff;
1063            time.normalize(true /* ignore isDst */);
1064        }
1065    }
1066
1067    @Override
1068    protected void onSizeChanged(int width, int height, int oldw, int oldh) {
1069        mViewWidth = width;
1070        mViewHeight = height;
1071        mEdgeEffectTop.setSize(mViewWidth, mViewHeight);
1072        mEdgeEffectBottom.setSize(mViewWidth, mViewHeight);
1073        int gridAreaWidth = width - mHoursWidth;
1074        mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
1075
1076        // This would be about 1 day worth in a 7 day view
1077        mHorizontalSnapBackThreshold = width / 7;
1078
1079        Paint p = new Paint();
1080        p.setTextSize(HOURS_TEXT_SIZE);
1081        mHoursTextHeight = (int) Math.abs(p.ascent());
1082        remeasure(width, height);
1083    }
1084
1085    /**
1086     * Measures the space needed for various parts of the view after
1087     * loading new events.  This can change if there are all-day events.
1088     */
1089    private void remeasure(int width, int height) {
1090        // Shrink to fit available space but make sure we can display at least two events
1091        MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4);
1092        MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6);
1093        MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT,
1094                (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2);
1095        mMaxUnexpandedAlldayEventCount =
1096                (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1097
1098        // First, clear the array of earliest start times, and the array
1099        // indicating presence of an all-day event.
1100        for (int day = 0; day < mNumDays; day++) {
1101            mEarliestStartHour[day] = 25;  // some big number
1102            mHasAllDayEvent[day] = false;
1103        }
1104
1105        int maxAllDayEvents = mMaxAlldayEvents;
1106
1107        // The min is where 24 hours cover the entire visible area
1108        mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT);
1109        if (mCellHeight < mMinCellHeight) {
1110            mCellHeight = mMinCellHeight;
1111        }
1112
1113        // Calculate mAllDayHeight
1114        mFirstCell = DAY_HEADER_HEIGHT;
1115        int allDayHeight = 0;
1116        if (maxAllDayEvents > 0) {
1117            int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
1118            // If there is at most one all-day event per day, then use less
1119            // space (but more than the space for a single event).
1120            if (maxAllDayEvents == 1) {
1121                allDayHeight = SINGLE_ALLDAY_HEIGHT;
1122            } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){
1123                // Allow the all-day area to grow in height depending on the
1124                // number of all-day events we need to show, up to a limit.
1125                allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
1126                if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
1127                    allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT;
1128                }
1129            } else {
1130                // if we have more than the magic number, check if we're animating
1131                // and if not adjust the sizes appropriately
1132                if (mAnimateDayHeight != 0) {
1133                    // Don't shrink the space past the final allDay space. The animation
1134                    // continues to hide the last event so the more events text can
1135                    // fade in.
1136                    allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT);
1137                } else {
1138                    // Try to fit all the events in
1139                    allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1140                    // But clip the area depending on which mode we're in
1141                    if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
1142                        allDayHeight = (int) (mMaxUnexpandedAlldayEventCount *
1143                                MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1144                    } else if (allDayHeight > maxAllAllDayHeight) {
1145                        allDayHeight = maxAllAllDayHeight;
1146                    }
1147                }
1148            }
1149            mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN;
1150        } else {
1151            mSelectionAllday = false;
1152        }
1153        mAlldayHeight = allDayHeight;
1154
1155        mGridAreaHeight = height - mFirstCell;
1156
1157        // Set up the expand icon position
1158        int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth();
1159        mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2,
1160                EVENT_ALL_DAY_TEXT_LEFT_MARGIN);
1161        mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth
1162                - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN);
1163        mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN;
1164        mExpandAllDayRect.top = mExpandAllDayRect.bottom
1165                - mExpandAlldayDrawable.getIntrinsicHeight();
1166
1167        mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP);
1168        mEventGeometry.setHourHeight(mCellHeight);
1169
1170        final long minimumDurationMillis = (long)
1171                (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f));
1172        Event.computePositions(mEvents, minimumDurationMillis);
1173
1174        // Compute the top of our reachable view
1175        mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight;
1176        if (DEBUG) {
1177            Log.e(TAG, "mViewStartY: " + mViewStartY);
1178            Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY);
1179        }
1180        if (mViewStartY > mMaxViewStartY) {
1181            mViewStartY = mMaxViewStartY;
1182            computeFirstHour();
1183        }
1184
1185        if (mFirstHour == -1) {
1186            initFirstHour();
1187            mFirstHourOffset = 0;
1188        }
1189
1190        // When we change the base date, the number of all-day events may
1191        // change and that changes the cell height.  When we switch dates,
1192        // we use the mFirstHourOffset from the previous view, but that may
1193        // be too large for the new view if the cell height is smaller.
1194        if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
1195            mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
1196        }
1197        mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
1198
1199        final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
1200        //When we get new events we don't want to dismiss the popup unless the event changes
1201        if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
1202            mPopup.dismiss();
1203        }
1204        mPopup.setWidth(eventAreaWidth - 20);
1205        mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
1206    }
1207
1208    /**
1209     * Initialize the state for another view.  The given view is one that has
1210     * its own bitmap and will use an animation to replace the current view.
1211     * The current view and new view are either both Week views or both Day
1212     * views.  They differ in their base date.
1213     *
1214     * @param view the view to initialize.
1215     */
1216    private void initView(DayView view) {
1217        view.mSelectionHour = mSelectionHour;
1218        view.mSelectedEvents.clear();
1219        view.mComputeSelectedEvents = true;
1220        view.mFirstHour = mFirstHour;
1221        view.mFirstHourOffset = mFirstHourOffset;
1222        view.remeasure(getWidth(), getHeight());
1223        view.initAllDayHeights();
1224
1225        view.mSelectedEvent = null;
1226        view.mPrevSelectedEvent = null;
1227        view.mFirstDayOfWeek = mFirstDayOfWeek;
1228        if (view.mEvents.size() > 0) {
1229            view.mSelectionAllday = mSelectionAllday;
1230        } else {
1231            view.mSelectionAllday = false;
1232        }
1233
1234        // Redraw the screen so that the selection box will be redrawn.  We may
1235        // have scrolled to a different part of the day in some other view
1236        // so the selection box in this view may no longer be visible.
1237        view.recalc();
1238    }
1239
1240    /**
1241     * Switch to another view based on what was selected (an event or a free
1242     * slot) and how it was selected (by touch or by trackball).
1243     *
1244     * @param trackBallSelection true if the selection was made using the
1245     * trackball.
1246     */
1247    private void switchViews(boolean trackBallSelection) {
1248        Event selectedEvent = mSelectedEvent;
1249
1250        mPopup.dismiss();
1251        mLastPopupEventID = INVALID_EVENT_ID;
1252        if (mNumDays > 1) {
1253            // This is the Week view.
1254            // With touch, we always switch to Day/Agenda View
1255            // With track ball, if we selected a free slot, then create an event.
1256            // If we selected a specific event, switch to EventInfo view.
1257            if (trackBallSelection) {
1258                if (selectedEvent == null) {
1259                    // Switch to the EditEvent view
1260                    long startMillis = getSelectedTimeInMillis();
1261                    long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
1262                    long extraLong = 0;
1263                    if (mSelectionAllday) {
1264                        extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
1265                    }
1266                    mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1,
1267                            startMillis, endMillis, -1, -1, extraLong, -1);
1268                } else {
1269                    if (mIsAccessibilityEnabled) {
1270                        mAccessibilityMgr.interrupt();
1271                    }
1272                    // Switch to the EventInfo view
1273                    mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1274                            selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1275                            getSelectedTimeInMillis());
1276                }
1277            } else {
1278                // This was a touch selection.  If the touch selected a single
1279                // unambiguous event, then view that event.  Otherwise go to
1280                // Day/Agenda view.
1281                if (mSelectedEvents.size() == 1) {
1282                    if (mIsAccessibilityEnabled) {
1283                        mAccessibilityMgr.interrupt();
1284                    }
1285                    mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1286                            selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1287                            getSelectedTimeInMillis());
1288                }
1289            }
1290        } else {
1291            // This is the Day view.
1292            // If we selected a free slot, then create an event.
1293            // If we selected an event, then go to the EventInfo view.
1294            if (selectedEvent == null) {
1295                // Switch to the EditEvent view
1296                long startMillis = getSelectedTimeInMillis();
1297                long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
1298                long extraLong = 0;
1299                if (mSelectionAllday) {
1300                    extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
1301                }
1302                mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1,
1303                        startMillis, endMillis, -1, -1, extraLong, -1);
1304            } else {
1305                if (mIsAccessibilityEnabled) {
1306                    mAccessibilityMgr.interrupt();
1307                }
1308                mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1309                        selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1310                        getSelectedTimeInMillis());
1311            }
1312        }
1313    }
1314
1315    @Override
1316    public boolean onKeyUp(int keyCode, KeyEvent event) {
1317        mScrolling = false;
1318        long duration = event.getEventTime() - event.getDownTime();
1319
1320        switch (keyCode) {
1321            case KeyEvent.KEYCODE_DPAD_CENTER:
1322                if (mSelectionMode == SELECTION_HIDDEN) {
1323                    // Don't do anything unless the selection is visible.
1324                    break;
1325                }
1326
1327                if (mSelectionMode == SELECTION_PRESSED) {
1328                    // This was the first press when there was nothing selected.
1329                    // Change the selection from the "pressed" state to the
1330                    // the "selected" state.  We treat short-press and
1331                    // long-press the same here because nothing was selected.
1332                    mSelectionMode = SELECTION_SELECTED;
1333                    invalidate();
1334                    break;
1335                }
1336
1337                // Check the duration to determine if this was a short press
1338                if (duration < ViewConfiguration.getLongPressTimeout()) {
1339                    switchViews(true /* trackball */);
1340                } else {
1341                    mSelectionMode = SELECTION_LONGPRESS;
1342                    invalidate();
1343                    performLongClick();
1344                }
1345                break;
1346//            case KeyEvent.KEYCODE_BACK:
1347//                if (event.isTracking() && !event.isCanceled()) {
1348//                    mPopup.dismiss();
1349//                    mContext.finish();
1350//                    return true;
1351//                }
1352//                break;
1353        }
1354        return super.onKeyUp(keyCode, event);
1355    }
1356
1357    @Override
1358    public boolean onKeyDown(int keyCode, KeyEvent event) {
1359        if (mSelectionMode == SELECTION_HIDDEN) {
1360            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1361                    || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
1362                    || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
1363                // Display the selection box but don't move or select it
1364                // on this key press.
1365                mSelectionMode = SELECTION_SELECTED;
1366                invalidate();
1367                return true;
1368            } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
1369                // Display the selection box but don't select it
1370                // on this key press.
1371                mSelectionMode = SELECTION_PRESSED;
1372                invalidate();
1373                return true;
1374            }
1375        }
1376
1377        mSelectionMode = SELECTION_SELECTED;
1378        mScrolling = false;
1379        boolean redraw;
1380        int selectionDay = mSelectionDay;
1381
1382        switch (keyCode) {
1383            case KeyEvent.KEYCODE_DEL:
1384                // Delete the selected event, if any
1385                Event selectedEvent = mSelectedEvent;
1386                if (selectedEvent == null) {
1387                    return false;
1388                }
1389                mPopup.dismiss();
1390                mLastPopupEventID = INVALID_EVENT_ID;
1391
1392                long begin = selectedEvent.startMillis;
1393                long end = selectedEvent.endMillis;
1394                long id = selectedEvent.id;
1395                mDeleteEventHelper.delete(begin, end, id, -1);
1396                return true;
1397            case KeyEvent.KEYCODE_ENTER:
1398                switchViews(true /* trackball or keyboard */);
1399                return true;
1400            case KeyEvent.KEYCODE_BACK:
1401                if (event.getRepeatCount() == 0) {
1402                    event.startTracking();
1403                    return true;
1404                }
1405                return super.onKeyDown(keyCode, event);
1406            case KeyEvent.KEYCODE_DPAD_LEFT:
1407                if (mSelectedEvent != null) {
1408                    mSelectedEvent = mSelectedEvent.nextLeft;
1409                }
1410                if (mSelectedEvent == null) {
1411                    mLastPopupEventID = INVALID_EVENT_ID;
1412                    selectionDay -= 1;
1413                }
1414                redraw = true;
1415                break;
1416
1417            case KeyEvent.KEYCODE_DPAD_RIGHT:
1418                if (mSelectedEvent != null) {
1419                    mSelectedEvent = mSelectedEvent.nextRight;
1420                }
1421                if (mSelectedEvent == null) {
1422                    mLastPopupEventID = INVALID_EVENT_ID;
1423                    selectionDay += 1;
1424                }
1425                redraw = true;
1426                break;
1427
1428            case KeyEvent.KEYCODE_DPAD_UP:
1429                if (mSelectedEvent != null) {
1430                    mSelectedEvent = mSelectedEvent.nextUp;
1431                }
1432                if (mSelectedEvent == null) {
1433                    mLastPopupEventID = INVALID_EVENT_ID;
1434                    if (!mSelectionAllday) {
1435                        mSelectionHour -= 1;
1436                        adjustHourSelection();
1437                        mSelectedEvents.clear();
1438                        mComputeSelectedEvents = true;
1439                    }
1440                }
1441                redraw = true;
1442                break;
1443
1444            case KeyEvent.KEYCODE_DPAD_DOWN:
1445                if (mSelectedEvent != null) {
1446                    mSelectedEvent = mSelectedEvent.nextDown;
1447                }
1448                if (mSelectedEvent == null) {
1449                    mLastPopupEventID = INVALID_EVENT_ID;
1450                    if (mSelectionAllday) {
1451                        mSelectionAllday = false;
1452                    } else {
1453                        mSelectionHour++;
1454                        adjustHourSelection();
1455                        mSelectedEvents.clear();
1456                        mComputeSelectedEvents = true;
1457                    }
1458                }
1459                redraw = true;
1460                break;
1461
1462            default:
1463                return super.onKeyDown(keyCode, event);
1464        }
1465
1466        if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1467            DayView view = (DayView) mViewSwitcher.getNextView();
1468            Time date = view.mBaseDate;
1469            date.set(mBaseDate);
1470            if (selectionDay < mFirstJulianDay) {
1471                date.monthDay -= mNumDays;
1472            } else {
1473                date.monthDay += mNumDays;
1474            }
1475            date.normalize(true /* ignore isDst */);
1476            view.mSelectionDay = selectionDay;
1477
1478            initView(view);
1479
1480            Time end = new Time(date);
1481            end.monthDay += mNumDays - 1;
1482            mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT);
1483            return true;
1484        }
1485        if (mSelectionDay != selectionDay) {
1486            Time date = new Time(mBaseDate);
1487            date.setJulianDay(selectionDay);
1488            date.hour = mSelectionHour;
1489            mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT);
1490        }
1491        mSelectionDay = selectionDay;
1492        mSelectedEvents.clear();
1493        mComputeSelectedEvents = true;
1494        mUpdateToast = true;
1495
1496        if (redraw) {
1497            invalidate();
1498            return true;
1499        }
1500
1501        return super.onKeyDown(keyCode, event);
1502    }
1503
1504
1505    @Override
1506    public boolean onHoverEvent(MotionEvent event) {
1507        if (DEBUG) {
1508            int action = event.getAction();
1509            switch (action) {
1510                case MotionEvent.ACTION_HOVER_ENTER:
1511                    Log.e(TAG, "ACTION_HOVER_ENTER");
1512                    break;
1513                case MotionEvent.ACTION_HOVER_MOVE:
1514                    Log.e(TAG, "ACTION_HOVER_MOVE");
1515                    break;
1516                case MotionEvent.ACTION_HOVER_EXIT:
1517                    Log.e(TAG, "ACTION_HOVER_EXIT");
1518                    break;
1519                default:
1520                    Log.e(TAG, "Unknown hover event action. " + event);
1521            }
1522        }
1523
1524        // Mouse also generates hover events
1525        // Send accessibility events if accessibility and exploration are on.
1526        if (!mTouchExplorationEnabled) {
1527            return super.onHoverEvent(event);
1528        }
1529        if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
1530            setSelectionFromPosition((int) event.getX(), (int) event.getY());
1531            invalidate();
1532        }
1533        return true;
1534    }
1535
1536    private boolean isTouchExplorationEnabled() {
1537        return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled();
1538    }
1539
1540    private void sendAccessibilityEventAsNeeded(boolean speakEvents) {
1541        if (!mIsAccessibilityEnabled) {
1542            return;
1543        }
1544        boolean dayChanged = mLastSelectionDay != mSelectionDay;
1545        boolean hourChanged = mLastSelectionHour != mSelectionHour;
1546        if (dayChanged || hourChanged || mLastSelectedEvent != mSelectedEvent) {
1547            mLastSelectionDay = mSelectionDay;
1548            mLastSelectionHour = mSelectionHour;
1549            mLastSelectedEvent = mSelectedEvent;
1550
1551            StringBuilder b = new StringBuilder();
1552
1553            // Announce only the changes i.e. day or hour or both
1554            if (dayChanged) {
1555                b.append(getSelectedTime().format("%A "));
1556            }
1557            if (hourChanged) {
1558                b.append(getSelectedTime().format(mIs24HourFormat ? "%k" : "%l%p"));
1559            }
1560            if (dayChanged || hourChanged) {
1561                b.append(PERIOD_SPACE);
1562            }
1563
1564            if (speakEvents) {
1565                if (mEventCountTemplate == null) {
1566                    mEventCountTemplate = mContext.getString(R.string.template_announce_item_index);
1567                }
1568
1569                // Read out the relevant event(s)
1570                int numEvents = mSelectedEvents.size();
1571                if (numEvents > 0) {
1572                    if (mSelectedEvent == null) {
1573                        // Read out all the events
1574                        int i = 1;
1575                        for (Event calEvent : mSelectedEvents) {
1576                            if (numEvents > 1) {
1577                                // Read out x of numEvents if there are more than one event
1578                                mStringBuilder.setLength(0);
1579                                b.append(mFormatter.format(mEventCountTemplate, i++, numEvents));
1580                                b.append(" ");
1581                            }
1582                            appendEventAccessibilityString(b, calEvent);
1583                        }
1584                    } else {
1585                        if (numEvents > 1) {
1586                            // Read out x of numEvents if there are more than one event
1587                            mStringBuilder.setLength(0);
1588                            b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents
1589                                    .indexOf(mSelectedEvent) + 1, numEvents));
1590                            b.append(" ");
1591                        }
1592                        appendEventAccessibilityString(b, mSelectedEvent);
1593                    }
1594                } else {
1595                    b.append(mCreateNewEventString);
1596                }
1597            }
1598
1599            if (dayChanged || hourChanged || speakEvents) {
1600                AccessibilityEvent event = AccessibilityEvent
1601                        .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
1602                CharSequence msg = b.toString();
1603                event.getText().add(msg);
1604                event.setAddedCount(msg.length());
1605                sendAccessibilityEventUnchecked(event);
1606            }
1607        }
1608    }
1609
1610    /**
1611     * @param b
1612     * @param calEvent
1613     */
1614    private void appendEventAccessibilityString(StringBuilder b, Event calEvent) {
1615        b.append(calEvent.getTitleAndLocation());
1616        b.append(PERIOD_SPACE);
1617        String when;
1618        int flags = DateUtils.FORMAT_SHOW_DATE;
1619        if (calEvent.allDay) {
1620            flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY;
1621        } else {
1622            flags |= DateUtils.FORMAT_SHOW_TIME;
1623            if (DateFormat.is24HourFormat(mContext)) {
1624                flags |= DateUtils.FORMAT_24HOUR;
1625            }
1626        }
1627        when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags);
1628        b.append(when);
1629        b.append(PERIOD_SPACE);
1630    }
1631
1632    private class GotoBroadcaster implements Animation.AnimationListener {
1633        private final int mCounter;
1634        private final Time mStart;
1635        private final Time mEnd;
1636
1637        public GotoBroadcaster(Time start, Time end) {
1638            mCounter = ++sCounter;
1639            mStart = start;
1640            mEnd = end;
1641        }
1642
1643        @Override
1644        public void onAnimationEnd(Animation animation) {
1645            DayView view = (DayView) mViewSwitcher.getCurrentView();
1646            view.mViewStartX = 0;
1647            view = (DayView) mViewSwitcher.getNextView();
1648            view.mViewStartX = 0;
1649
1650            if (mCounter == sCounter) {
1651                mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1,
1652                        ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null);
1653            }
1654        }
1655
1656        @Override
1657        public void onAnimationRepeat(Animation animation) {
1658        }
1659
1660        @Override
1661        public void onAnimationStart(Animation animation) {
1662        }
1663    }
1664
1665    private View switchViews(boolean forward, float xOffSet, float width, float velocity) {
1666        mAnimationDistance = width - xOffSet;
1667        if (DEBUG) {
1668            Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance);
1669        }
1670
1671        float progress = Math.abs(xOffSet) / width;
1672        if (progress > 1.0f) {
1673            progress = 1.0f;
1674        }
1675
1676        float inFromXValue, inToXValue;
1677        float outFromXValue, outToXValue;
1678        if (forward) {
1679            inFromXValue = 1.0f - progress;
1680            inToXValue = 0.0f;
1681            outFromXValue = -progress;
1682            outToXValue = -1.0f;
1683        } else {
1684            inFromXValue = progress - 1.0f;
1685            inToXValue = 0.0f;
1686            outFromXValue = progress;
1687            outToXValue = 1.0f;
1688        }
1689
1690        final Time start = new Time(mBaseDate.timezone);
1691        start.set(mController.getTime());
1692        if (forward) {
1693            start.monthDay += mNumDays;
1694        } else {
1695            start.monthDay -= mNumDays;
1696        }
1697        mController.setTime(start.normalize(true));
1698
1699        Time newSelected = start;
1700
1701        if (mNumDays == 7) {
1702            newSelected = new Time(start);
1703            adjustToBeginningOfWeek(start);
1704        }
1705
1706        final Time end = new Time(start);
1707        end.monthDay += mNumDays - 1;
1708
1709        // We have to allocate these animation objects each time we switch views
1710        // because that is the only way to set the animation parameters.
1711        TranslateAnimation inAnimation = new TranslateAnimation(
1712                Animation.RELATIVE_TO_SELF, inFromXValue,
1713                Animation.RELATIVE_TO_SELF, inToXValue,
1714                Animation.ABSOLUTE, 0.0f,
1715                Animation.ABSOLUTE, 0.0f);
1716
1717        TranslateAnimation outAnimation = new TranslateAnimation(
1718                Animation.RELATIVE_TO_SELF, outFromXValue,
1719                Animation.RELATIVE_TO_SELF, outToXValue,
1720                Animation.ABSOLUTE, 0.0f,
1721                Animation.ABSOLUTE, 0.0f);
1722
1723        long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity);
1724        inAnimation.setDuration(duration);
1725        inAnimation.setInterpolator(mHScrollInterpolator);
1726        outAnimation.setInterpolator(mHScrollInterpolator);
1727        outAnimation.setDuration(duration);
1728        outAnimation.setAnimationListener(new GotoBroadcaster(start, end));
1729        mViewSwitcher.setInAnimation(inAnimation);
1730        mViewSwitcher.setOutAnimation(outAnimation);
1731
1732        DayView view = (DayView) mViewSwitcher.getCurrentView();
1733        view.cleanup();
1734        mViewSwitcher.showNext();
1735        view = (DayView) mViewSwitcher.getCurrentView();
1736        view.setSelected(newSelected, true);
1737        view.requestFocus();
1738        view.reloadEvents();
1739        view.updateTitle();
1740
1741        return view;
1742    }
1743
1744    // This is called after scrolling stops to move the selected hour
1745    // to the visible part of the screen.
1746    private void resetSelectedHour() {
1747        if (mSelectionHour < mFirstHour + 1) {
1748            mSelectionHour = mFirstHour + 1;
1749            mSelectedEvent = null;
1750            mSelectedEvents.clear();
1751            mComputeSelectedEvents = true;
1752        } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1753            mSelectionHour = mFirstHour + mNumHours - 3;
1754            mSelectedEvent = null;
1755            mSelectedEvents.clear();
1756            mComputeSelectedEvents = true;
1757        }
1758    }
1759
1760    private void initFirstHour() {
1761        mFirstHour = mSelectionHour - mNumHours / 5;
1762        if (mFirstHour < 0) {
1763            mFirstHour = 0;
1764        } else if (mFirstHour + mNumHours > 24) {
1765            mFirstHour = 24 - mNumHours;
1766        }
1767    }
1768
1769    /**
1770     * Recomputes the first full hour that is visible on screen after the
1771     * screen is scrolled.
1772     */
1773    private void computeFirstHour() {
1774        // Compute the first full hour that is visible on screen
1775        mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1776        mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1777    }
1778
1779    private void adjustHourSelection() {
1780        if (mSelectionHour < 0) {
1781            mSelectionHour = 0;
1782            if (mMaxAlldayEvents > 0) {
1783                mPrevSelectedEvent = null;
1784                mSelectionAllday = true;
1785            }
1786        }
1787
1788        if (mSelectionHour > 23) {
1789            mSelectionHour = 23;
1790        }
1791
1792        // If the selected hour is at least 2 time slots from the top and
1793        // bottom of the screen, then don't scroll the view.
1794        if (mSelectionHour < mFirstHour + 1) {
1795            // If there are all-days events for the selected day but there
1796            // are no more normal events earlier in the day, then jump to
1797            // the all-day event area.
1798            // Exception 1: allow the user to scroll to 8am with the trackball
1799            // before jumping to the all-day event area.
1800            // Exception 2: if 12am is on screen, then allow the user to select
1801            // 12am before going up to the all-day event area.
1802            int daynum = mSelectionDay - mFirstJulianDay;
1803            if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1804                    && mFirstHour > 0 && mFirstHour < 8) {
1805                mPrevSelectedEvent = null;
1806                mSelectionAllday = true;
1807                mSelectionHour = mFirstHour + 1;
1808                return;
1809            }
1810
1811            if (mFirstHour > 0) {
1812                mFirstHour -= 1;
1813                mViewStartY -= (mCellHeight + HOUR_GAP);
1814                if (mViewStartY < 0) {
1815                    mViewStartY = 0;
1816                }
1817                return;
1818            }
1819        }
1820
1821        if (mSelectionHour > mFirstHour + mNumHours - 3) {
1822            if (mFirstHour < 24 - mNumHours) {
1823                mFirstHour += 1;
1824                mViewStartY += (mCellHeight + HOUR_GAP);
1825                if (mViewStartY > mMaxViewStartY) {
1826                    mViewStartY = mMaxViewStartY;
1827                }
1828                return;
1829            } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1830                mViewStartY = mMaxViewStartY;
1831            }
1832        }
1833    }
1834
1835    void clearCachedEvents() {
1836        mLastReloadMillis = 0;
1837    }
1838
1839    private Runnable mCancelCallback = new Runnable() {
1840        public void run() {
1841            clearCachedEvents();
1842        }
1843    };
1844
1845    /* package */ void reloadEvents() {
1846        // Protect against this being called before this view has been
1847        // initialized.
1848//        if (mContext == null) {
1849//            return;
1850//        }
1851
1852        // Make sure our time zones are up to date
1853        mTZUpdater.run();
1854
1855        mSelectedEvent = null;
1856        mPrevSelectedEvent = null;
1857        mSelectedEvents.clear();
1858
1859        // The start date is the beginning of the week at 12am
1860        Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater));
1861        weekStart.set(mBaseDate);
1862        weekStart.hour = 0;
1863        weekStart.minute = 0;
1864        weekStart.second = 0;
1865        long millis = weekStart.normalize(true /* ignore isDst */);
1866
1867        // Avoid reloading events unnecessarily.
1868        if (millis == mLastReloadMillis) {
1869            return;
1870        }
1871        mLastReloadMillis = millis;
1872
1873        // load events in the background
1874//        mContext.startProgressSpinner();
1875        final ArrayList<Event> events = new ArrayList<Event>();
1876        mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() {
1877            public void run() {
1878                mEvents = events;
1879                if (mAllDayEvents == null) {
1880                    mAllDayEvents = new ArrayList<Event>();
1881                } else {
1882                    mAllDayEvents.clear();
1883                }
1884
1885                // Create a shorter array for all day events
1886                for (Event e : events) {
1887                    if (e.drawAsAllday()) {
1888                        mAllDayEvents.add(e);
1889                    }
1890                }
1891
1892                // New events, new layouts
1893                if (mLayouts == null || mLayouts.length < events.size()) {
1894                    mLayouts = new StaticLayout[events.size()];
1895                } else {
1896                    Arrays.fill(mLayouts, null);
1897                }
1898
1899                if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) {
1900                    mAllDayLayouts = new StaticLayout[events.size()];
1901                } else {
1902                    Arrays.fill(mAllDayLayouts, null);
1903                }
1904
1905                computeEventRelations();
1906
1907                mRemeasure = true;
1908                mComputeSelectedEvents = true;
1909                recalc();
1910//                mContext.stopProgressSpinner();
1911                invalidate();
1912            }
1913        }, mCancelCallback);
1914    }
1915
1916    private void computeEventRelations() {
1917        // Compute the layout relation between each event before measuring cell
1918        // width, as the cell width should be adjusted along with the relation.
1919        //
1920        // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm)
1921        // We should mark them as "overwapped". Though they are not overwapped logically, but
1922        // minimum cell height implicitly expands the cell height of A and it should look like
1923        // (1:00pm - 1:15pm) after the cell height adjustment.
1924
1925        // Compute the space needed for the all-day events, if any.
1926        // Make a pass over all the events, and keep track of the maximum
1927        // number of all-day events in any one day.  Also, keep track of
1928        // the earliest event in each day.
1929        int maxAllDayEvents = 0;
1930        final ArrayList<Event> events = mEvents;
1931        final int len = events.size();
1932        // Num of all-day-events on each day.
1933        final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1];
1934        Arrays.fill(eventsCount, 0);
1935        for (int ii = 0; ii < len; ii++) {
1936            Event event = events.get(ii);
1937            if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) {
1938                continue;
1939            }
1940            if (event.drawAsAllday()) {
1941                // Count all the events being drawn as allDay events
1942                final int firstDay = Math.max(event.startDay, mFirstJulianDay);
1943                final int lastDay = Math.min(event.endDay, mLastJulianDay);
1944                for (int day = firstDay; day <= lastDay; day++) {
1945                    final int count = ++eventsCount[day - mFirstJulianDay];
1946                    if (maxAllDayEvents < count) {
1947                        maxAllDayEvents = count;
1948                    }
1949                }
1950
1951                int daynum = event.startDay - mFirstJulianDay;
1952                int durationDays = event.endDay - event.startDay + 1;
1953                if (daynum < 0) {
1954                    durationDays += daynum;
1955                    daynum = 0;
1956                }
1957                if (daynum + durationDays > mNumDays) {
1958                    durationDays = mNumDays - daynum;
1959                }
1960                for (int day = daynum; durationDays > 0; day++, durationDays--) {
1961                    mHasAllDayEvent[day] = true;
1962                }
1963            } else {
1964                int daynum = event.startDay - mFirstJulianDay;
1965                int hour = event.startTime / 60;
1966                if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
1967                    mEarliestStartHour[daynum] = hour;
1968                }
1969
1970                // Also check the end hour in case the event spans more than
1971                // one day.
1972                daynum = event.endDay - mFirstJulianDay;
1973                hour = event.endTime / 60;
1974                if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
1975                    mEarliestStartHour[daynum] = hour;
1976                }
1977            }
1978        }
1979        mMaxAlldayEvents = maxAllDayEvents;
1980        initAllDayHeights();
1981    }
1982
1983    @Override
1984    protected void onDraw(Canvas canvas) {
1985        if (mRemeasure) {
1986            remeasure(getWidth(), getHeight());
1987            mRemeasure = false;
1988        }
1989        canvas.save();
1990
1991        float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight;
1992        // offset canvas by the current drag and header position
1993        canvas.translate(-mViewStartX, yTranslate);
1994        // clip to everything below the allDay area
1995        Rect dest = mDestRect;
1996        dest.top = (int) (mFirstCell - yTranslate);
1997        dest.bottom = (int) (mViewHeight - yTranslate);
1998        dest.left = 0;
1999        dest.right = mViewWidth;
2000        canvas.save();
2001        canvas.clipRect(dest);
2002        // Draw the movable part of the view
2003        doDraw(canvas);
2004        // restore to having no clip
2005        canvas.restore();
2006
2007        if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2008            float xTranslate;
2009            if (mViewStartX > 0) {
2010                xTranslate = mViewWidth;
2011            } else {
2012                xTranslate = -mViewWidth;
2013            }
2014            // Move the canvas around to prep it for the next view
2015            // specifically, shift it by a screen and undo the
2016            // yTranslation which will be redone in the nextView's onDraw().
2017            canvas.translate(xTranslate, -yTranslate);
2018            DayView nextView = (DayView) mViewSwitcher.getNextView();
2019
2020            // Prevent infinite recursive calls to onDraw().
2021            nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
2022
2023            nextView.onDraw(canvas);
2024            // Move it back for this view
2025            canvas.translate(-xTranslate, 0);
2026        } else {
2027            // If we drew another view we already translated it back
2028            // If we didn't draw another view we should be at the edge of the
2029            // screen
2030            canvas.translate(mViewStartX, -yTranslate);
2031        }
2032
2033        // Draw the fixed areas (that don't scroll) directly to the canvas.
2034        drawAfterScroll(canvas);
2035        if (mComputeSelectedEvents && mUpdateToast) {
2036            updateEventDetails();
2037            mUpdateToast = false;
2038        }
2039        mComputeSelectedEvents = false;
2040
2041        // Draw overscroll glow
2042        if (!mEdgeEffectTop.isFinished()) {
2043            if (DAY_HEADER_HEIGHT != 0) {
2044                canvas.translate(0, DAY_HEADER_HEIGHT);
2045            }
2046            if (mEdgeEffectTop.draw(canvas)) {
2047                invalidate();
2048            }
2049            if (DAY_HEADER_HEIGHT != 0) {
2050                canvas.translate(0, -DAY_HEADER_HEIGHT);
2051            }
2052        }
2053        if (!mEdgeEffectBottom.isFinished()) {
2054            canvas.rotate(180, mViewWidth/2, mViewHeight/2);
2055            if (mEdgeEffectBottom.draw(canvas)) {
2056                invalidate();
2057            }
2058        }
2059        canvas.restore();
2060    }
2061
2062    private void drawAfterScroll(Canvas canvas) {
2063        Paint p = mPaint;
2064        Rect r = mRect;
2065
2066        drawAllDayHighlights(r, canvas, p);
2067        if (mMaxAlldayEvents != 0) {
2068            drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p);
2069            drawUpperLeftCorner(r, canvas, p);
2070        }
2071
2072        drawScrollLine(r, canvas, p);
2073        drawDayHeaderLoop(r, canvas, p);
2074
2075        // Draw the AM and PM indicators if we're in 12 hour mode
2076        if (!mIs24HourFormat) {
2077            drawAmPm(canvas, p);
2078        }
2079    }
2080
2081    // This isn't really the upper-left corner. It's the square area just
2082    // below the upper-left corner, above the hours and to the left of the
2083    // all-day area.
2084    private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
2085        setupHourTextPaint(p);
2086        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2087            // Draw the allDay expand/collapse icon
2088            if (mUseExpandIcon) {
2089                mExpandAlldayDrawable.setBounds(mExpandAllDayRect);
2090                mExpandAlldayDrawable.draw(canvas);
2091            } else {
2092                mCollapseAlldayDrawable.setBounds(mExpandAllDayRect);
2093                mCollapseAlldayDrawable.draw(canvas);
2094            }
2095        }
2096    }
2097
2098    private void drawScrollLine(Rect r, Canvas canvas, Paint p) {
2099        final int right = computeDayLeftPosition(mNumDays + 1);
2100        final int y = mFirstCell - 1;
2101
2102        p.setAntiAlias(false);
2103        p.setStyle(Style.FILL);
2104
2105        p.setColor(mCalendarGridLineInnerHorizontalColor);
2106        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2107        canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p);
2108        p.setAntiAlias(true);
2109    }
2110
2111    // Computes the x position for the left side of the given day
2112    private int computeDayLeftPosition(int day) {
2113        int effectiveWidth = mViewWidth - mHoursWidth;
2114        return day * effectiveWidth / mNumDays + mHoursWidth;
2115    }
2116
2117    private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) {
2118        // First, color the labels area light gray
2119        r.top = 0;
2120        r.bottom = DAY_HEADER_HEIGHT;
2121        r.left = 0;
2122        r.right = mViewWidth;
2123        p.setColor(mBgColor);
2124        p.setStyle(Style.FILL);
2125        canvas.drawRect(r, p);
2126        // and the area that says All day
2127        r.top = DAY_HEADER_HEIGHT;
2128        r.bottom = mFirstCell - 1;
2129        r.left = 0;
2130        r.right = mHoursWidth;
2131        canvas.drawRect(r, p);
2132
2133        int startIndex = -1;
2134        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2135        if (todayIndex < 0) {
2136            // Future
2137            startIndex = 0;
2138        } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) {
2139            // Multiday - tomorrow is visible.
2140            startIndex = todayIndex + 1;
2141        }
2142
2143        if (startIndex >= 0) {
2144            // Draw the future highlight
2145            r.top = 0;
2146            r.bottom = mFirstCell - 1;
2147            r.left = computeDayLeftPosition(startIndex) + 1;
2148            r.right = computeDayLeftPosition(mNumDays + 1);
2149            p.setColor(mFutureBgColor);
2150            p.setStyle(Style.FILL);
2151            canvas.drawRect(r, p);
2152        }
2153
2154        if (mSelectionAllday) {
2155            // Draw the selection highlight on the selected all-day area
2156            mRect.top = DAY_HEADER_HEIGHT + 1;
2157            mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2;
2158            int daynum = mSelectionDay - mFirstJulianDay;
2159            mRect.left = computeDayLeftPosition(daynum) + 1;
2160            mRect.right = computeDayLeftPosition(daynum + 1);
2161            p.setColor(mCalendarGridAreaSelected);
2162            canvas.drawRect(mRect, p);
2163        }
2164    }
2165
2166    private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
2167        // Draw the horizontal day background banner
2168        // p.setColor(mCalendarDateBannerBackground);
2169        // r.top = 0;
2170        // r.bottom = DAY_HEADER_HEIGHT;
2171        // r.left = 0;
2172        // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
2173        // canvas.drawRect(r, p);
2174        //
2175        // Fill the extra space on the right side with the default background
2176        // r.left = r.right;
2177        // r.right = mViewWidth;
2178        // p.setColor(mCalendarGridAreaBackground);
2179        // canvas.drawRect(r, p);
2180        if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) {
2181            return;
2182        }
2183
2184        p.setTypeface(mBold);
2185        p.setTextAlign(Paint.Align.RIGHT);
2186        int cell = mFirstJulianDay;
2187
2188        String[] dayNames;
2189        if (mDateStrWidth < mCellWidth) {
2190            dayNames = mDayStrs;
2191        } else {
2192            dayNames = mDayStrs2Letter;
2193        }
2194
2195        p.setAntiAlias(true);
2196        for (int day = 0; day < mNumDays; day++, cell++) {
2197            int dayOfWeek = day + mFirstVisibleDayOfWeek;
2198            if (dayOfWeek >= 14) {
2199                dayOfWeek -= 14;
2200            }
2201
2202            int color = mCalendarDateBannerTextColor;
2203            if (mNumDays == 1) {
2204                if (dayOfWeek == Time.SATURDAY) {
2205                    color = mWeek_saturdayColor;
2206                } else if (dayOfWeek == Time.SUNDAY) {
2207                    color = mWeek_sundayColor;
2208                }
2209            } else {
2210                final int column = day % 7;
2211                if (Utils.isSaturday(column, mFirstDayOfWeek)) {
2212                    color = mWeek_saturdayColor;
2213                } else if (Utils.isSunday(column, mFirstDayOfWeek)) {
2214                    color = mWeek_sundayColor;
2215                }
2216            }
2217
2218            p.setColor(color);
2219            drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p);
2220        }
2221        p.setTypeface(null);
2222    }
2223
2224    private void drawAmPm(Canvas canvas, Paint p) {
2225        p.setColor(mCalendarAmPmLabel);
2226        p.setTextSize(AMPM_TEXT_SIZE);
2227        p.setTypeface(mBold);
2228        p.setAntiAlias(true);
2229        p.setTextAlign(Paint.Align.RIGHT);
2230        String text = mAmString;
2231        if (mFirstHour >= 12) {
2232            text = mPmString;
2233        }
2234        int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
2235        canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2236
2237        if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
2238            // Also draw the "PM"
2239            text = mPmString;
2240            y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
2241                    + 2 * mHoursTextHeight + HOUR_GAP;
2242            canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2243        }
2244    }
2245
2246    private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas,
2247            Paint p) {
2248        r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1;
2249        r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1;
2250
2251        r.top = top - CURRENT_TIME_LINE_TOP_OFFSET;
2252        r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight();
2253
2254        mCurrentTimeLine.setBounds(r);
2255        mCurrentTimeLine.draw(canvas);
2256    }
2257
2258    private void doDraw(Canvas canvas) {
2259        Paint p = mPaint;
2260        Rect r = mRect;
2261
2262        drawBgColors(r, canvas, p);
2263        drawGridBackground(r, canvas, p);
2264        drawHours(r, canvas, p);
2265
2266        // Draw each day
2267        int cell = mFirstJulianDay;
2268        p.setAntiAlias(false);
2269        for (int day = 0; day < mNumDays; day++, cell++) {
2270            // TODO Wow, this needs cleanup. drawEvents loop through all the
2271            // events on every call.
2272            drawEvents(cell, day, HOUR_GAP, canvas, p);
2273            // If this is today
2274            if (cell == mTodayJulianDay) {
2275                int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2276                        + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2277
2278                // And the current time shows up somewhere on the screen
2279                if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
2280                    drawCurrentTimeLine(r, day, lineY, canvas, p);
2281                }
2282            }
2283        }
2284        p.setAntiAlias(true);
2285
2286        drawSelectedRect(r, canvas, p);
2287    }
2288
2289    private void drawSelectedRect(Rect r, Canvas canvas, Paint p) {
2290        // Draw a highlight on the selected hour (if needed)
2291        if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) {
2292            int daynum = mSelectionDay - mFirstJulianDay;
2293            r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2294            r.bottom = r.top + mCellHeight + HOUR_GAP;
2295            r.left = computeDayLeftPosition(daynum) + 1;
2296            r.right = computeDayLeftPosition(daynum + 1) + 1;
2297
2298            saveSelectionPosition(r.left, r.top, r.right, r.bottom);
2299
2300            // Draw the highlight on the grid
2301            p.setColor(mCalendarGridAreaSelected);
2302            r.top += HOUR_GAP;
2303            r.right -= DAY_GAP;
2304            p.setAntiAlias(false);
2305            canvas.drawRect(r, p);
2306
2307            // Draw a + on top of the highlight
2308            int width = r.right - r.left;
2309            int midX = r.left + width / 2;
2310            int midY = r.top + mCellHeight / 2;
2311            int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2;
2312            length = Math.min(length, NEW_EVENT_MAX_LENGTH);
2313            int verticalPadding = (mCellHeight - length) / 2;
2314            int horizontalPadding = (width - length) / 2;
2315            p.setStrokeWidth(NEW_EVENT_WIDTH);
2316            p.setColor(0xFFFFFFFF);
2317            canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, midY, p);
2318            canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p);
2319        }
2320    }
2321
2322    private void drawHours(Rect r, Canvas canvas, Paint p) {
2323        setupHourTextPaint(p);
2324
2325        int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN;
2326
2327        for (int i = 0; i < 24; i++) {
2328            String time = mHourStrs[i];
2329            canvas.drawText(time, HOURS_LEFT_MARGIN, y, p);
2330            y += mCellHeight + HOUR_GAP;
2331        }
2332    }
2333
2334    private void setupHourTextPaint(Paint p) {
2335        p.setColor(mCalendarHourLabelColor);
2336        p.setTextSize(HOURS_TEXT_SIZE);
2337        p.setTypeface(Typeface.DEFAULT);
2338        p.setTextAlign(Paint.Align.RIGHT);
2339        p.setAntiAlias(true);
2340    }
2341
2342    private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) {
2343        int dateNum = mFirstVisibleDate + day;
2344        int x;
2345        if (dateNum > mMonthLength) {
2346            dateNum -= mMonthLength;
2347        }
2348        p.setAntiAlias(true);
2349
2350        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2351        // Draw day of the month
2352        String dateNumStr = String.valueOf(dateNum);
2353        if (mNumDays > 1) {
2354            float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN;
2355
2356            // Draw day of the month
2357            x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN;
2358            p.setTextAlign(Align.RIGHT);
2359            p.setTextSize(DATE_HEADER_FONT_SIZE);
2360
2361            p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT);
2362            canvas.drawText(dateNumStr, x, y, p);
2363
2364            // Draw day of the week
2365            x -= p.measureText(" " + dateNumStr);
2366            p.setTextSize(DAY_HEADER_FONT_SIZE);
2367            p.setTypeface(Typeface.DEFAULT);
2368            canvas.drawText(dayStr, x, y, p);
2369        } else {
2370            float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN;
2371            p.setTextAlign(Align.LEFT);
2372
2373
2374            // Draw day of the week
2375            x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN;
2376            p.setTextSize(DAY_HEADER_FONT_SIZE);
2377            p.setTypeface(Typeface.DEFAULT);
2378            canvas.drawText(dayStr, x, y, p);
2379
2380            // Draw day of the month
2381            x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN;
2382            p.setTextSize(DATE_HEADER_FONT_SIZE);
2383            p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT);
2384            canvas.drawText(dateNumStr, x, y, p);
2385        }
2386    }
2387
2388    private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
2389        Paint.Style savedStyle = p.getStyle();
2390
2391        final float stopX = computeDayLeftPosition(mNumDays + 1);
2392        float y = 0;
2393        final float deltaY = mCellHeight + HOUR_GAP;
2394        int linesIndex = 0;
2395        final float startY = 0;
2396        final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
2397        float x = mHoursWidth;
2398
2399        // Draw the inner horizontal grid lines
2400        p.setColor(mCalendarGridLineInnerHorizontalColor);
2401        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2402        p.setAntiAlias(false);
2403        y = 0;
2404        linesIndex = 0;
2405        for (int hour = 0; hour <= 24; hour++) {
2406            mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2407            mLines[linesIndex++] = y;
2408            mLines[linesIndex++] = stopX;
2409            mLines[linesIndex++] = y;
2410            y += deltaY;
2411        }
2412        if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) {
2413            canvas.drawLines(mLines, 0, linesIndex, p);
2414            linesIndex = 0;
2415            p.setColor(mCalendarGridLineInnerVerticalColor);
2416        }
2417
2418        // Draw the inner vertical grid lines
2419        for (int day = 0; day <= mNumDays; day++) {
2420            x = computeDayLeftPosition(day);
2421            mLines[linesIndex++] = x;
2422            mLines[linesIndex++] = startY;
2423            mLines[linesIndex++] = x;
2424            mLines[linesIndex++] = stopY;
2425        }
2426        canvas.drawLines(mLines, 0, linesIndex, p);
2427
2428        // Restore the saved style.
2429        p.setStyle(savedStyle);
2430        p.setAntiAlias(true);
2431    }
2432
2433    /**
2434     * @param r
2435     * @param canvas
2436     * @param p
2437     */
2438    private void drawBgColors(Rect r, Canvas canvas, Paint p) {
2439        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2440        // Draw the hours background color
2441        r.top = mDestRect.top;
2442        r.bottom = mDestRect.bottom;
2443        r.left = 0;
2444        r.right = mHoursWidth;
2445        p.setColor(mBgColor);
2446        p.setStyle(Style.FILL);
2447        p.setAntiAlias(false);
2448        canvas.drawRect(r, p);
2449
2450        // Draw background for grid area
2451        if (mNumDays == 1 && todayIndex == 0) {
2452            // Draw a white background for the time later than current time
2453            int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2454                    + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2455            if (lineY < mViewStartY + mViewHeight) {
2456                lineY = Math.max(lineY, mViewStartY);
2457                r.left = mHoursWidth;
2458                r.right = mViewWidth;
2459                r.top = lineY;
2460                r.bottom = mViewStartY + mViewHeight;
2461                p.setColor(mFutureBgColor);
2462                canvas.drawRect(r, p);
2463            }
2464        } else if (todayIndex >= 0 && todayIndex < mNumDays) {
2465            // Draw today with a white background for the time later than current time
2466            int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2467                    + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2468            if (lineY < mViewStartY + mViewHeight) {
2469                lineY = Math.max(lineY, mViewStartY);
2470                r.left = computeDayLeftPosition(todayIndex) + 1;
2471                r.right = computeDayLeftPosition(todayIndex + 1);
2472                r.top = lineY;
2473                r.bottom = mViewStartY + mViewHeight;
2474                p.setColor(mFutureBgColor);
2475                canvas.drawRect(r, p);
2476            }
2477
2478            // Paint Tomorrow and later days with future color
2479            if (todayIndex + 1 < mNumDays) {
2480                r.left = computeDayLeftPosition(todayIndex + 1) + 1;
2481                r.right = computeDayLeftPosition(mNumDays + 1);
2482                r.top = mDestRect.top;
2483                r.bottom = mDestRect.bottom;
2484                p.setColor(mFutureBgColor);
2485                canvas.drawRect(r, p);
2486            }
2487        } else if (todayIndex < 0) {
2488            // Future
2489            r.left = computeDayLeftPosition(0) + 1;
2490            r.right = computeDayLeftPosition(mNumDays + 1);
2491            r.top = mDestRect.top;
2492            r.bottom = mDestRect.bottom;
2493            p.setColor(mFutureBgColor);
2494            canvas.drawRect(r, p);
2495        }
2496        p.setAntiAlias(true);
2497    }
2498
2499    Event getSelectedEvent() {
2500        if (mSelectedEvent == null) {
2501            // There is no event at the selected hour, so create a new event.
2502            return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2503                    getSelectedMinutesSinceMidnight());
2504        }
2505        return mSelectedEvent;
2506    }
2507
2508    boolean isEventSelected() {
2509        return (mSelectedEvent != null);
2510    }
2511
2512    Event getNewEvent() {
2513        return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2514                getSelectedMinutesSinceMidnight());
2515    }
2516
2517    static Event getNewEvent(int julianDay, long utcMillis,
2518            int minutesSinceMidnight) {
2519        Event event = Event.newInstance();
2520        event.startDay = julianDay;
2521        event.endDay = julianDay;
2522        event.startMillis = utcMillis;
2523        event.endMillis = event.startMillis + MILLIS_PER_HOUR;
2524        event.startTime = minutesSinceMidnight;
2525        event.endTime = event.startTime + MINUTES_PER_HOUR;
2526        return event;
2527    }
2528
2529    private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
2530        float maxWidthF = 0.0f;
2531
2532        int len = strings.length;
2533        for (int i = 0; i < len; i++) {
2534            float width = p.measureText(strings[i]);
2535            maxWidthF = Math.max(width, maxWidthF);
2536        }
2537        int maxWidth = (int) (maxWidthF + 0.5);
2538        if (maxWidth < currentMax) {
2539            maxWidth = currentMax;
2540        }
2541        return maxWidth;
2542    }
2543
2544    private void saveSelectionPosition(float left, float top, float right, float bottom) {
2545        mPrevBox.left = (int) left;
2546        mPrevBox.right = (int) right;
2547        mPrevBox.top = (int) top;
2548        mPrevBox.bottom = (int) bottom;
2549    }
2550
2551    private Rect getCurrentSelectionPosition() {
2552        Rect box = new Rect();
2553        box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2554        box.bottom = box.top + mCellHeight + HOUR_GAP;
2555        int daynum = mSelectionDay - mFirstJulianDay;
2556        box.left = computeDayLeftPosition(daynum) + 1;
2557        box.right = computeDayLeftPosition(daynum + 1);
2558        return box;
2559    }
2560
2561    private void setupTextRect(Rect r) {
2562        if (r.bottom <= r.top || r.right <= r.left) {
2563            r.bottom = r.top;
2564            r.right = r.left;
2565            return;
2566        }
2567
2568        if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) {
2569            r.top += EVENT_TEXT_TOP_MARGIN;
2570            r.bottom -= EVENT_TEXT_BOTTOM_MARGIN;
2571        }
2572        if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) {
2573            r.left += EVENT_TEXT_LEFT_MARGIN;
2574            r.right -= EVENT_TEXT_RIGHT_MARGIN;
2575        }
2576    }
2577
2578    private void setupAllDayTextRect(Rect r) {
2579        if (r.bottom <= r.top || r.right <= r.left) {
2580            r.bottom = r.top;
2581            r.right = r.left;
2582            return;
2583        }
2584
2585        if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) {
2586            r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN;
2587            r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN;
2588        }
2589        if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) {
2590            r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN;
2591            r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN;
2592        }
2593    }
2594
2595    /**
2596     * Return the layout for a numbered event. Create it if not already existing
2597     */
2598    private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint,
2599            Rect r) {
2600        if (i < 0 || i >= layouts.length) {
2601            return null;
2602        }
2603
2604        StaticLayout layout = layouts[i];
2605        // Check if we have already initialized the StaticLayout and that
2606        // the width hasn't changed (due to vertical resizing which causes
2607        // re-layout of events at min height)
2608        if (layout == null || r.width() != layout.getWidth()) {
2609            SpannableStringBuilder bob = new SpannableStringBuilder();
2610            if (event.title != null) {
2611                bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN));
2612                bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0);
2613                bob.append(' ');
2614            }
2615            if (event.location != null) {
2616                bob.append(drawTextSanitizer(event.location.toString(),
2617                        MAX_EVENT_TEXT_LEN - bob.length()));
2618            }
2619
2620            switch (event.selfAttendeeStatus) {
2621                case Attendees.ATTENDEE_STATUS_INVITED:
2622                    paint.setColor(event.color);
2623                    break;
2624                case Attendees.ATTENDEE_STATUS_DECLINED:
2625                case Attendees.ATTENDEE_STATUS_NONE: // Your own events
2626                case Attendees.ATTENDEE_STATUS_ACCEPTED:
2627                case Attendees.ATTENDEE_STATUS_TENTATIVE:
2628                default:
2629                    paint.setColor(mEventTextColor);
2630                    break;
2631            }
2632
2633            // Leave a one pixel boundary on the left and right of the rectangle for the event
2634            layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(),
2635                    Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width());
2636
2637            layouts[i] = layout;
2638        }
2639
2640        return layout;
2641    }
2642
2643    private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) {
2644
2645        p.setTextSize(NORMAL_FONT_SIZE);
2646        p.setTextAlign(Paint.Align.LEFT);
2647        Paint eventTextPaint = mEventTextPaint;
2648
2649        final float startY = DAY_HEADER_HEIGHT;
2650        final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN;
2651        float x = 0;
2652        int linesIndex = 0;
2653
2654        // Draw the inner vertical grid lines
2655        p.setColor(mCalendarGridLineInnerVerticalColor);
2656        x = mHoursWidth;
2657        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2658        // Line bounding the top of the all day area
2659        mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2660        mLines[linesIndex++] = startY;
2661        mLines[linesIndex++] = computeDayLeftPosition(mNumDays + 1);
2662        mLines[linesIndex++] = startY;
2663
2664        for (int day = 0; day <= mNumDays; day++) {
2665            x = computeDayLeftPosition(day);
2666            mLines[linesIndex++] = x;
2667            mLines[linesIndex++] = startY;
2668            mLines[linesIndex++] = x;
2669            mLines[linesIndex++] = stopY;
2670        }
2671        p.setAntiAlias(false);
2672        canvas.drawLines(mLines, 0, linesIndex, p);
2673        p.setStyle(Style.FILL);
2674
2675        int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
2676        int lastDay = firstDay + numDays - 1;
2677        final ArrayList<Event> events = mAllDayEvents;
2678        int numEvents = events.size();
2679        // Whether or not we should draw the more events text
2680        boolean hasMoreEvents = false;
2681        // size of the allDay area
2682        float drawHeight = mAlldayHeight;
2683        // max number of events being drawn in one day of the allday area
2684        float numRectangles = mMaxAlldayEvents;
2685        // Where to cut off drawn allday events
2686        int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN;
2687        // The number of events that weren't drawn in each day
2688        mSkippedAlldayEvents = new int[numDays];
2689        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents &&
2690                mAnimateDayHeight == 0) {
2691            // We draw one fewer event than will fit so that more events text
2692            // can be drawn
2693            numRectangles = mMaxUnexpandedAlldayEventCount - 1;
2694            // We also clip the events above the more events text
2695            allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
2696            hasMoreEvents = true;
2697        } else if (mAnimateDayHeight != 0) {
2698            // clip at the end of the animating space
2699            allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN;
2700        }
2701        for (int i = 0; i < numEvents; i++) {
2702            Event event = events.get(i);
2703            int startDay = event.startDay;
2704            int endDay = event.endDay;
2705            if (startDay > lastDay || endDay < firstDay) {
2706                continue;
2707            }
2708            if (startDay < firstDay) {
2709                startDay = firstDay;
2710            }
2711            if (endDay > lastDay) {
2712                endDay = lastDay;
2713            }
2714            int startIndex = startDay - firstDay;
2715            int endIndex = endDay - firstDay;
2716            float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight :
2717                    drawHeight / numRectangles;
2718
2719            // Prevent a single event from getting too big
2720            if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
2721                height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
2722            }
2723
2724            // Leave a one-pixel space between the vertical day lines and the
2725            // event rectangle.
2726            event.left = computeDayLeftPosition(startIndex);
2727            event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP;
2728            event.top = y + height * event.getColumn();
2729            event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN;
2730            if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2731                // check if we should skip this event. We skip if it starts
2732                // after the clip bound or ends after the skip bound and we're
2733                // not animating.
2734                if (event.top >= allDayEventClip) {
2735                    incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2736                    continue;
2737                } else if (event.bottom > allDayEventClip) {
2738                    if (hasMoreEvents) {
2739                        incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2740                        continue;
2741                    }
2742                    event.bottom = allDayEventClip;
2743                }
2744            }
2745            Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top,
2746                    (int) event.bottom);
2747            setupAllDayTextRect(r);
2748            StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r);
2749            drawEventText(layout, r, canvas, r.top, r.bottom);
2750
2751            // Check if this all-day event intersects the selected day
2752            if (mSelectionAllday && mComputeSelectedEvents) {
2753                if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
2754                    mSelectedEvents.add(event);
2755                }
2756            }
2757        }
2758        if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) {
2759            // If the more allday text should be visible, draw it.
2760            p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor);
2761            for (int i = 0; i < mSkippedAlldayEvents.length; i++) {
2762                if (mSkippedAlldayEvents[i] > 0) {
2763                    drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p);
2764                }
2765            }
2766        }
2767
2768        if (mSelectionAllday) {
2769            // Compute the neighbors for the list of all-day events that
2770            // intersect the selected day.
2771            computeAllDayNeighbors();
2772
2773            // Set the selection position to zero so that when we move down
2774            // to the normal event area, we will highlight the topmost event.
2775            saveSelectionPosition(0f, 0f, 0f, 0f);
2776        }
2777    }
2778
2779    // Helper method for counting the number of allday events skipped on each day
2780    private void incrementSkipCount(int[] counts, int startIndex, int endIndex) {
2781        if (counts == null || startIndex < 0 || endIndex > counts.length) {
2782            return;
2783        }
2784        for (int i = startIndex; i <= endIndex; i++) {
2785            counts[i]++;
2786        }
2787    }
2788
2789    // Draws the "box +n" text for hidden allday events
2790    protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) {
2791        int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN;
2792        int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f
2793                * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN);
2794        Rect r = mRect;
2795        r.top = y;
2796        r.left = x;
2797        r.bottom = y + EVENT_SQUARE_WIDTH;
2798        r.right = x + EVENT_SQUARE_WIDTH;
2799        p.setColor(mMoreEventsTextColor);
2800        p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH);
2801        p.setStyle(Style.STROKE);
2802        p.setAntiAlias(false);
2803        canvas.drawRect(r, p);
2804        p.setAntiAlias(true);
2805        p.setStyle(Style.FILL);
2806        p.setTextSize(EVENT_TEXT_FONT_SIZE);
2807        String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents);
2808        y += EVENT_SQUARE_WIDTH;
2809        x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING;
2810        canvas.drawText(String.format(text, remainingEvents), x, y, p);
2811    }
2812
2813    private void computeAllDayNeighbors() {
2814        int len = mSelectedEvents.size();
2815        if (len == 0 || mSelectedEvent != null) {
2816            return;
2817        }
2818
2819        // First, clear all the links
2820        for (int ii = 0; ii < len; ii++) {
2821            Event ev = mSelectedEvents.get(ii);
2822            ev.nextUp = null;
2823            ev.nextDown = null;
2824            ev.nextLeft = null;
2825            ev.nextRight = null;
2826        }
2827
2828        // For each event in the selected event list "mSelectedEvents", find
2829        // its neighbors in the up and down directions. This could be done
2830        // more efficiently by sorting on the Event.getColumn() field, but
2831        // the list is expected to be very small.
2832
2833        // Find the event in the same row as the previously selected all-day
2834        // event, if any.
2835        int startPosition = -1;
2836        if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) {
2837            startPosition = mPrevSelectedEvent.getColumn();
2838        }
2839        int maxPosition = -1;
2840        Event startEvent = null;
2841        Event maxPositionEvent = null;
2842        for (int ii = 0; ii < len; ii++) {
2843            Event ev = mSelectedEvents.get(ii);
2844            int position = ev.getColumn();
2845            if (position == startPosition) {
2846                startEvent = ev;
2847            } else if (position > maxPosition) {
2848                maxPositionEvent = ev;
2849                maxPosition = position;
2850            }
2851            for (int jj = 0; jj < len; jj++) {
2852                if (jj == ii) {
2853                    continue;
2854                }
2855                Event neighbor = mSelectedEvents.get(jj);
2856                int neighborPosition = neighbor.getColumn();
2857                if (neighborPosition == position - 1) {
2858                    ev.nextUp = neighbor;
2859                } else if (neighborPosition == position + 1) {
2860                    ev.nextDown = neighbor;
2861                }
2862            }
2863        }
2864        if (startEvent != null) {
2865            mSelectedEvent = startEvent;
2866        } else {
2867            mSelectedEvent = maxPositionEvent;
2868        }
2869    }
2870
2871    private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) {
2872        Paint eventTextPaint = mEventTextPaint;
2873        int left = computeDayLeftPosition(dayIndex) + 1;
2874        int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1;
2875        int cellHeight = mCellHeight;
2876
2877        // Use the selected hour as the selection region
2878        Rect selectionArea = mSelectionRect;
2879        selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
2880        selectionArea.bottom = selectionArea.top + cellHeight;
2881        selectionArea.left = left;
2882        selectionArea.right = selectionArea.left + cellWidth;
2883
2884        final ArrayList<Event> events = mEvents;
2885        int numEvents = events.size();
2886        EventGeometry geometry = mEventGeometry;
2887
2888        final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight;
2889        for (int i = 0; i < numEvents; i++) {
2890            Event event = events.get(i);
2891            if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
2892                continue;
2893            }
2894
2895            // Don't draw it if it is not visible
2896            if (event.bottom < mViewStartY || event.top > viewEndY) {
2897                continue;
2898            }
2899
2900            if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents
2901                    && geometry.eventIntersectsSelection(event, selectionArea)) {
2902                mSelectedEvents.add(event);
2903            }
2904
2905            Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY);
2906            setupTextRect(r);
2907
2908            // Don't draw text if it is not visible
2909            if (r.top > viewEndY || r.bottom < mViewStartY) {
2910                continue;
2911            }
2912            StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r);
2913            // TODO: not sure why we are 4 pixels off
2914            drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight
2915                    - DAY_HEADER_HEIGHT - mAlldayHeight);
2916        }
2917
2918        if (date == mSelectionDay && !mSelectionAllday && isFocused()
2919                && mSelectionMode != SELECTION_HIDDEN) {
2920            computeNeighbors();
2921        }
2922    }
2923
2924    // Computes the "nearest" neighbor event in four directions (left, right,
2925    // up, down) for each of the events in the mSelectedEvents array.
2926    private void computeNeighbors() {
2927        int len = mSelectedEvents.size();
2928        if (len == 0 || mSelectedEvent != null) {
2929            return;
2930        }
2931
2932        // First, clear all the links
2933        for (int ii = 0; ii < len; ii++) {
2934            Event ev = mSelectedEvents.get(ii);
2935            ev.nextUp = null;
2936            ev.nextDown = null;
2937            ev.nextLeft = null;
2938            ev.nextRight = null;
2939        }
2940
2941        Event startEvent = mSelectedEvents.get(0);
2942        int startEventDistance1 = 100000; // any large number
2943        int startEventDistance2 = 100000; // any large number
2944        int prevLocation = FROM_NONE;
2945        int prevTop;
2946        int prevBottom;
2947        int prevLeft;
2948        int prevRight;
2949        int prevCenter = 0;
2950        Rect box = getCurrentSelectionPosition();
2951        if (mPrevSelectedEvent != null) {
2952            prevTop = (int) mPrevSelectedEvent.top;
2953            prevBottom = (int) mPrevSelectedEvent.bottom;
2954            prevLeft = (int) mPrevSelectedEvent.left;
2955            prevRight = (int) mPrevSelectedEvent.right;
2956            // Check if the previously selected event intersects the previous
2957            // selection box. (The previously selected event may be from a
2958            // much older selection box.)
2959            if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
2960                    || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
2961                mPrevSelectedEvent = null;
2962                prevTop = mPrevBox.top;
2963                prevBottom = mPrevBox.bottom;
2964                prevLeft = mPrevBox.left;
2965                prevRight = mPrevBox.right;
2966            } else {
2967                // Clip the top and bottom to the previous selection box.
2968                if (prevTop < mPrevBox.top) {
2969                    prevTop = mPrevBox.top;
2970                }
2971                if (prevBottom > mPrevBox.bottom) {
2972                    prevBottom = mPrevBox.bottom;
2973                }
2974            }
2975        } else {
2976            // Just use the previously drawn selection box
2977            prevTop = mPrevBox.top;
2978            prevBottom = mPrevBox.bottom;
2979            prevLeft = mPrevBox.left;
2980            prevRight = mPrevBox.right;
2981        }
2982
2983        // Figure out where we came from and compute the center of that area.
2984        if (prevLeft >= box.right) {
2985            // The previously selected event was to the right of us.
2986            prevLocation = FROM_RIGHT;
2987            prevCenter = (prevTop + prevBottom) / 2;
2988        } else if (prevRight <= box.left) {
2989            // The previously selected event was to the left of us.
2990            prevLocation = FROM_LEFT;
2991            prevCenter = (prevTop + prevBottom) / 2;
2992        } else if (prevBottom <= box.top) {
2993            // The previously selected event was above us.
2994            prevLocation = FROM_ABOVE;
2995            prevCenter = (prevLeft + prevRight) / 2;
2996        } else if (prevTop >= box.bottom) {
2997            // The previously selected event was below us.
2998            prevLocation = FROM_BELOW;
2999            prevCenter = (prevLeft + prevRight) / 2;
3000        }
3001
3002        // For each event in the selected event list "mSelectedEvents", search
3003        // all the other events in that list for the nearest neighbor in 4
3004        // directions.
3005        for (int ii = 0; ii < len; ii++) {
3006            Event ev = mSelectedEvents.get(ii);
3007
3008            int startTime = ev.startTime;
3009            int endTime = ev.endTime;
3010            int left = (int) ev.left;
3011            int right = (int) ev.right;
3012            int top = (int) ev.top;
3013            if (top < box.top) {
3014                top = box.top;
3015            }
3016            int bottom = (int) ev.bottom;
3017            if (bottom > box.bottom) {
3018                bottom = box.bottom;
3019            }
3020//            if (false) {
3021//                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
3022//                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
3023//                if (DateFormat.is24HourFormat(mContext)) {
3024//                    flags |= DateUtils.FORMAT_24HOUR;
3025//                }
3026//                String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis,
3027//                        ev.endMillis, flags);
3028//                Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: "
3029//                        + bottom + " ev: " + timeRange + " " + ev.title);
3030//            }
3031            int upDistanceMin = 10000; // any large number
3032            int downDistanceMin = 10000; // any large number
3033            int leftDistanceMin = 10000; // any large number
3034            int rightDistanceMin = 10000; // any large number
3035            Event upEvent = null;
3036            Event downEvent = null;
3037            Event leftEvent = null;
3038            Event rightEvent = null;
3039
3040            // Pick the starting event closest to the previously selected event,
3041            // if any. distance1 takes precedence over distance2.
3042            int distance1 = 0;
3043            int distance2 = 0;
3044            if (prevLocation == FROM_ABOVE) {
3045                if (left >= prevCenter) {
3046                    distance1 = left - prevCenter;
3047                } else if (right <= prevCenter) {
3048                    distance1 = prevCenter - right;
3049                }
3050                distance2 = top - prevBottom;
3051            } else if (prevLocation == FROM_BELOW) {
3052                if (left >= prevCenter) {
3053                    distance1 = left - prevCenter;
3054                } else if (right <= prevCenter) {
3055                    distance1 = prevCenter - right;
3056                }
3057                distance2 = prevTop - bottom;
3058            } else if (prevLocation == FROM_LEFT) {
3059                if (bottom <= prevCenter) {
3060                    distance1 = prevCenter - bottom;
3061                } else if (top >= prevCenter) {
3062                    distance1 = top - prevCenter;
3063                }
3064                distance2 = left - prevRight;
3065            } else if (prevLocation == FROM_RIGHT) {
3066                if (bottom <= prevCenter) {
3067                    distance1 = prevCenter - bottom;
3068                } else if (top >= prevCenter) {
3069                    distance1 = top - prevCenter;
3070                }
3071                distance2 = prevLeft - right;
3072            }
3073            if (distance1 < startEventDistance1
3074                    || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
3075                startEvent = ev;
3076                startEventDistance1 = distance1;
3077                startEventDistance2 = distance2;
3078            }
3079
3080            // For each neighbor, figure out if it is above or below or left
3081            // or right of me and compute the distance.
3082            for (int jj = 0; jj < len; jj++) {
3083                if (jj == ii) {
3084                    continue;
3085                }
3086                Event neighbor = mSelectedEvents.get(jj);
3087                int neighborLeft = (int) neighbor.left;
3088                int neighborRight = (int) neighbor.right;
3089                if (neighbor.endTime <= startTime) {
3090                    // This neighbor is entirely above me.
3091                    // If we overlap the same column, then compute the distance.
3092                    if (neighborLeft < right && neighborRight > left) {
3093                        int distance = startTime - neighbor.endTime;
3094                        if (distance < upDistanceMin) {
3095                            upDistanceMin = distance;
3096                            upEvent = neighbor;
3097                        } else if (distance == upDistanceMin) {
3098                            int center = (left + right) / 2;
3099                            int currentDistance = 0;
3100                            int currentLeft = (int) upEvent.left;
3101                            int currentRight = (int) upEvent.right;
3102                            if (currentRight <= center) {
3103                                currentDistance = center - currentRight;
3104                            } else if (currentLeft >= center) {
3105                                currentDistance = currentLeft - center;
3106                            }
3107
3108                            int neighborDistance = 0;
3109                            if (neighborRight <= center) {
3110                                neighborDistance = center - neighborRight;
3111                            } else if (neighborLeft >= center) {
3112                                neighborDistance = neighborLeft - center;
3113                            }
3114                            if (neighborDistance < currentDistance) {
3115                                upDistanceMin = distance;
3116                                upEvent = neighbor;
3117                            }
3118                        }
3119                    }
3120                } else if (neighbor.startTime >= endTime) {
3121                    // This neighbor is entirely below me.
3122                    // If we overlap the same column, then compute the distance.
3123                    if (neighborLeft < right && neighborRight > left) {
3124                        int distance = neighbor.startTime - endTime;
3125                        if (distance < downDistanceMin) {
3126                            downDistanceMin = distance;
3127                            downEvent = neighbor;
3128                        } else if (distance == downDistanceMin) {
3129                            int center = (left + right) / 2;
3130                            int currentDistance = 0;
3131                            int currentLeft = (int) downEvent.left;
3132                            int currentRight = (int) downEvent.right;
3133                            if (currentRight <= center) {
3134                                currentDistance = center - currentRight;
3135                            } else if (currentLeft >= center) {
3136                                currentDistance = currentLeft - center;
3137                            }
3138
3139                            int neighborDistance = 0;
3140                            if (neighborRight <= center) {
3141                                neighborDistance = center - neighborRight;
3142                            } else if (neighborLeft >= center) {
3143                                neighborDistance = neighborLeft - center;
3144                            }
3145                            if (neighborDistance < currentDistance) {
3146                                downDistanceMin = distance;
3147                                downEvent = neighbor;
3148                            }
3149                        }
3150                    }
3151                }
3152
3153                if (neighborLeft >= right) {
3154                    // This neighbor is entirely to the right of me.
3155                    // Take the closest neighbor in the y direction.
3156                    int center = (top + bottom) / 2;
3157                    int distance = 0;
3158                    int neighborBottom = (int) neighbor.bottom;
3159                    int neighborTop = (int) neighbor.top;
3160                    if (neighborBottom <= center) {
3161                        distance = center - neighborBottom;
3162                    } else if (neighborTop >= center) {
3163                        distance = neighborTop - center;
3164                    }
3165                    if (distance < rightDistanceMin) {
3166                        rightDistanceMin = distance;
3167                        rightEvent = neighbor;
3168                    } else if (distance == rightDistanceMin) {
3169                        // Pick the closest in the x direction
3170                        int neighborDistance = neighborLeft - right;
3171                        int currentDistance = (int) rightEvent.left - right;
3172                        if (neighborDistance < currentDistance) {
3173                            rightDistanceMin = distance;
3174                            rightEvent = neighbor;
3175                        }
3176                    }
3177                } else if (neighborRight <= left) {
3178                    // This neighbor is entirely to the left of me.
3179                    // Take the closest neighbor in the y direction.
3180                    int center = (top + bottom) / 2;
3181                    int distance = 0;
3182                    int neighborBottom = (int) neighbor.bottom;
3183                    int neighborTop = (int) neighbor.top;
3184                    if (neighborBottom <= center) {
3185                        distance = center - neighborBottom;
3186                    } else if (neighborTop >= center) {
3187                        distance = neighborTop - center;
3188                    }
3189                    if (distance < leftDistanceMin) {
3190                        leftDistanceMin = distance;
3191                        leftEvent = neighbor;
3192                    } else if (distance == leftDistanceMin) {
3193                        // Pick the closest in the x direction
3194                        int neighborDistance = left - neighborRight;
3195                        int currentDistance = left - (int) leftEvent.right;
3196                        if (neighborDistance < currentDistance) {
3197                            leftDistanceMin = distance;
3198                            leftEvent = neighbor;
3199                        }
3200                    }
3201                }
3202            }
3203            ev.nextUp = upEvent;
3204            ev.nextDown = downEvent;
3205            ev.nextLeft = leftEvent;
3206            ev.nextRight = rightEvent;
3207        }
3208        mSelectedEvent = startEvent;
3209    }
3210
3211    private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint,
3212            int visibleTop, int visibleBot) {
3213        // Draw the Event Rect
3214        Rect r = mRect;
3215        r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop);
3216        r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot);
3217        r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN;
3218        r.right = (int) event.right;
3219
3220        int color = event.color;
3221        switch (event.selfAttendeeStatus) {
3222            case Attendees.ATTENDEE_STATUS_INVITED:
3223                p.setStyle(Style.STROKE);
3224                break;
3225            case Attendees.ATTENDEE_STATUS_DECLINED:
3226                color = Utils.getDeclinedColorFromColor(color);
3227            case Attendees.ATTENDEE_STATUS_NONE: // Your own events
3228            case Attendees.ATTENDEE_STATUS_ACCEPTED:
3229            case Attendees.ATTENDEE_STATUS_TENTATIVE:
3230            default:
3231                p.setStyle(Style.FILL_AND_STROKE);
3232                break;
3233        }
3234
3235        p.setAntiAlias(false);
3236
3237        int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f);
3238        int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f);
3239        r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop);
3240        r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke,
3241                visibleBot);
3242        r.left += floorHalfStroke;
3243        r.right -= ceilHalfStroke;
3244        p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH);
3245        p.setColor(color);
3246        canvas.drawRect(r, p);
3247
3248        p.setStyle(Style.FILL);
3249
3250        // If this event is selected, then use the selection color
3251        if (mSelectedEvent == event) {
3252            boolean paintIt = false;
3253            color = 0;
3254            if (mSelectionMode == SELECTION_PRESSED) {
3255                // Also, remember the last selected event that we drew
3256                mPrevSelectedEvent = event;
3257                color = mPressedColor;
3258                paintIt = true;
3259            } else if (mSelectionMode == SELECTION_SELECTED) {
3260                // Also, remember the last selected event that we drew
3261                mPrevSelectedEvent = event;
3262                color = mPressedColor;
3263                paintIt = true;
3264            }
3265
3266            if (paintIt) {
3267                p.setColor(color);
3268                canvas.drawRect(r, p);
3269            }
3270
3271            p.setAntiAlias(true);
3272        }
3273
3274        // Draw cal color square border
3275        // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET;
3276        // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET;
3277        // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1;
3278        // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1;
3279        // p.setColor(0xFFFFFFFF);
3280        // canvas.drawRect(r, p);
3281
3282        // Draw cal color
3283        // r.top++;
3284        // r.left++;
3285        // r.bottom--;
3286        // r.right--;
3287        // p.setColor(event.color);
3288        // canvas.drawRect(r, p);
3289
3290        // Setup rect for drawEventText which follows
3291        r.top = (int) event.top + EVENT_RECT_TOP_MARGIN;
3292        r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN;
3293        r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN;
3294        r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN;
3295        return r;
3296    }
3297
3298    private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
3299
3300    // Sanitize a string before passing it to drawText or else we get little
3301    // squares. For newlines and tabs before a comma, delete the character.
3302    // Otherwise, just replace them with a space.
3303    private String drawTextSanitizer(String string, int maxEventTextLen) {
3304        Matcher m = drawTextSanitizerFilter.matcher(string);
3305        string = m.replaceAll(",");
3306
3307        int len = string.length();
3308        if (len > maxEventTextLen) {
3309            string = string.substring(0, maxEventTextLen);
3310            len = maxEventTextLen;
3311        }
3312
3313        return string.replace('\n', ' ');
3314    }
3315
3316    private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top,
3317            int bottom) {
3318        // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging
3319
3320        int width = rect.right - rect.left;
3321        int height = rect.bottom - rect.top;
3322
3323        // If the rectangle is too small for text, then return
3324        if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) {
3325            return;
3326        }
3327
3328        int totalLineHeight = 0;
3329        int lineCount = eventLayout.getLineCount();
3330        for (int i = 0; i < lineCount; i++) {
3331            int lineBottom = eventLayout.getLineBottom(i);
3332            if (lineBottom <= height) {
3333                totalLineHeight = lineBottom;
3334            } else {
3335                break;
3336            }
3337        }
3338
3339        if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) {
3340            return;
3341        }
3342
3343        // Use a StaticLayout to format the string.
3344        canvas.save();
3345        canvas.translate(rect.left, rect.top);
3346        rect.left = 0;
3347        rect.right = width;
3348        rect.top = 0;
3349        rect.bottom = totalLineHeight;
3350
3351        // There's a bug somewhere. If this rect is outside of a previous
3352        // cliprect, this becomes a no-op. What happens is that the text draw
3353        // past the event rect. The current fix is to not draw the staticLayout
3354        // at all if it is completely out of bound.
3355        canvas.clipRect(rect);
3356        eventLayout.draw(canvas);
3357        canvas.restore();
3358    }
3359
3360    // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it
3361    // doesn't work well with hardware acceleration
3362//    private void drawEmptyRect(Canvas canvas, Rect r, int color) {
3363//        int linesIndex = 0;
3364//        mLines[linesIndex++] = r.left;
3365//        mLines[linesIndex++] = r.top;
3366//        mLines[linesIndex++] = r.right;
3367//        mLines[linesIndex++] = r.top;
3368//
3369//        mLines[linesIndex++] = r.left;
3370//        mLines[linesIndex++] = r.bottom;
3371//        mLines[linesIndex++] = r.right;
3372//        mLines[linesIndex++] = r.bottom;
3373//
3374//        mLines[linesIndex++] = r.left;
3375//        mLines[linesIndex++] = r.top;
3376//        mLines[linesIndex++] = r.left;
3377//        mLines[linesIndex++] = r.bottom;
3378//
3379//        mLines[linesIndex++] = r.right;
3380//        mLines[linesIndex++] = r.top;
3381//        mLines[linesIndex++] = r.right;
3382//        mLines[linesIndex++] = r.bottom;
3383//        mPaint.setColor(color);
3384//        canvas.drawLines(mLines, 0, linesIndex, mPaint);
3385//    }
3386
3387    private void updateEventDetails() {
3388        if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
3389                || mSelectionMode == SELECTION_LONGPRESS) {
3390            mPopup.dismiss();
3391            return;
3392        }
3393        if (mLastPopupEventID == mSelectedEvent.id) {
3394            return;
3395        }
3396
3397        mLastPopupEventID = mSelectedEvent.id;
3398
3399        // Remove any outstanding callbacks to dismiss the popup.
3400        getHandler().removeCallbacks(mDismissPopup);
3401
3402        Event event = mSelectedEvent;
3403        TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
3404        titleView.setText(event.title);
3405
3406        ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
3407        imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
3408
3409        imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
3410        imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
3411
3412        int flags;
3413        if (event.allDay) {
3414            flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
3415                    | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
3416        } else {
3417            flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
3418                    | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
3419                    | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
3420        }
3421        if (DateFormat.is24HourFormat(mContext)) {
3422            flags |= DateUtils.FORMAT_24HOUR;
3423        }
3424        String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis,
3425                flags);
3426        TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
3427        timeView.setText(timeRange);
3428
3429        TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
3430        final boolean empty = TextUtils.isEmpty(event.location);
3431        whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
3432        if (!empty) whereView.setText(event.location);
3433
3434        mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
3435        postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
3436    }
3437
3438    // The following routines are called from the parent activity when certain
3439    // touch events occur.
3440    private void doDown(MotionEvent ev) {
3441        mTouchMode = TOUCH_MODE_DOWN;
3442        mViewStartX = 0;
3443        mOnFlingCalled = false;
3444        getHandler().removeCallbacks(mContinueScroll);
3445    }
3446
3447    // Kicks off all the animations when the expand allday area is tapped
3448    private void doExpandAllDayClick() {
3449        mShowAllAllDayEvents = !mShowAllAllDayEvents;
3450
3451        ObjectAnimator.setFrameDelay(0);
3452
3453        // Determine the starting height
3454        if (mAnimateDayHeight == 0) {
3455            mAnimateDayHeight = mShowAllAllDayEvents ?
3456                    mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight;
3457        }
3458        // Cancel current animations
3459        mCancellingAnimations = true;
3460        if (mAlldayAnimator != null) {
3461            mAlldayAnimator.cancel();
3462        }
3463        if (mAlldayEventAnimator != null) {
3464            mAlldayEventAnimator.cancel();
3465        }
3466        if (mMoreAlldayEventsAnimator != null) {
3467            mMoreAlldayEventsAnimator.cancel();
3468        }
3469        mCancellingAnimations = false;
3470        // get new animators
3471        mAlldayAnimator = getAllDayAnimator();
3472        mAlldayEventAnimator = getAllDayEventAnimator();
3473        mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this,
3474                    "moreAllDayEventsTextAlpha",
3475                    mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0,
3476                    mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA);
3477
3478        // Set up delays and start the animators
3479        mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0);
3480        mAlldayAnimator.start();
3481        mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION);
3482        mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION);
3483        mMoreAlldayEventsAnimator.start();
3484        if (mAlldayEventAnimator != null) {
3485            // This is the only animator that can return null, so check it
3486            mAlldayEventAnimator
3487                    .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0);
3488            mAlldayEventAnimator.start();
3489        }
3490    }
3491
3492    /**
3493     * Figures out the initial heights for allDay events and space when
3494     * a view is being set up.
3495     */
3496    public void initAllDayHeights() {
3497        if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) {
3498            return;
3499        }
3500        if (mShowAllAllDayEvents) {
3501            int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3502            maxADHeight = Math.min(maxADHeight,
3503                    (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3504            mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents;
3505        } else {
3506            mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
3507        }
3508    }
3509
3510    // Sets up an animator for changing the height of allday events
3511    private ObjectAnimator getAllDayEventAnimator() {
3512        // First calculate the absolute max height
3513        int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3514        // Now expand to fit but not beyond the absolute max
3515        maxADHeight =
3516                Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3517        // calculate the height of individual events in order to fit
3518        int fitHeight = maxADHeight / mMaxAlldayEvents;
3519        int currentHeight = mAnimateDayEventHeight;
3520        int desiredHeight =
3521                mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
3522        // if there's nothing to animate just return
3523        if (currentHeight == desiredHeight) {
3524            return null;
3525        }
3526
3527        // Set up the animator with the calculated values
3528        ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight",
3529                currentHeight, desiredHeight);
3530        animator.setDuration(ANIMATION_DURATION);
3531        return animator;
3532    }
3533
3534    // Sets up an animator for changing the height of the allday area
3535    private ObjectAnimator getAllDayAnimator() {
3536        // Calculate the absolute max height
3537        int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3538        // Find the desired height but don't exceed abs max
3539        maxADHeight =
3540                Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3541        // calculate the current and desired heights
3542        int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight;
3543        int desiredHeight = mShowAllAllDayEvents ? maxADHeight :
3544                (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1);
3545
3546        // Set up the animator with the calculated values
3547        ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight",
3548                currentHeight, desiredHeight);
3549        animator.setDuration(ANIMATION_DURATION);
3550
3551        animator.addListener(new AnimatorListenerAdapter() {
3552            @Override
3553            public void onAnimationEnd(Animator animation) {
3554                if (!mCancellingAnimations) {
3555                    // when finished, set this to 0 to signify not animating
3556                    mAnimateDayHeight = 0;
3557                    mUseExpandIcon = !mShowAllAllDayEvents;
3558                }
3559                mRemeasure = true;
3560                invalidate();
3561            }
3562        });
3563        return animator;
3564    }
3565
3566    // setter for the 'box +n' alpha text used by the animator
3567    public void setMoreAllDayEventsTextAlpha(int alpha) {
3568        mMoreAlldayEventsTextAlpha = alpha;
3569        invalidate();
3570    }
3571
3572    // setter for the height of the allday area used by the animator
3573    public void setAnimateDayHeight(int height) {
3574        mAnimateDayHeight = height;
3575        mRemeasure = true;
3576        invalidate();
3577    }
3578
3579    // setter for the height of allday events used by the animator
3580    public void setAnimateDayEventHeight(int height) {
3581        mAnimateDayEventHeight = height;
3582        mRemeasure = true;
3583        invalidate();
3584    }
3585
3586    private void doSingleTapUp(MotionEvent ev) {
3587        if (!mHandleActionUp || mScrolling) {
3588            return;
3589        }
3590
3591        int x = (int) ev.getX();
3592        int y = (int) ev.getY();
3593        int selectedDay = mSelectionDay;
3594        int selectedHour = mSelectionHour;
3595
3596        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
3597            // check if the tap was in the allday expansion area
3598            int bottom = mFirstCell;
3599            if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight)
3600                    || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom &&
3601                            y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) {
3602                doExpandAllDayClick();
3603                return;
3604            }
3605        }
3606
3607        boolean validPosition = setSelectionFromPosition(x, y);
3608        if (!validPosition) {
3609            if (y < DAY_HEADER_HEIGHT) {
3610                Time selectedTime = new Time(mBaseDate);
3611                selectedTime.setJulianDay(mSelectionDay);
3612                selectedTime.hour = mSelectionHour;
3613                selectedTime.normalize(true /* ignore isDst */);
3614                mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1,
3615                        ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null);
3616            }
3617            return;
3618        }
3619
3620        boolean hasSelection = mSelectionMode == SELECTION_SELECTED;
3621        boolean pressedSelected = (hasSelection || mTouchExplorationEnabled)
3622                && selectedDay == mSelectionDay && selectedHour == mSelectionHour;
3623
3624        if (pressedSelected) {
3625            // If the tap is on an already selected hour slot, then create a new
3626            // event
3627            long extraLong = 0;
3628            if (mSelectionAllday) {
3629                extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
3630            }
3631            mSelectionMode = SELECTION_SELECTED;
3632            mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1,
3633                    getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(),
3634                    extraLong, -1);
3635        } else if (mSelectedEvent != null) {
3636            // If the tap is on an event, launch the "View event" view
3637            if (mIsAccessibilityEnabled) {
3638                mAccessibilityMgr.interrupt();
3639            }
3640
3641            mSelectionMode = SELECTION_HIDDEN;
3642            mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mSelectedEvent.id,
3643                    mSelectedEvent.startMillis, mSelectedEvent.endMillis, (int) ev.getRawX(),
3644                    (int) ev.getRawY(), getSelectedTimeInMillis());
3645        } else {
3646            // Select time
3647            Time startTime = new Time(mBaseDate);
3648            startTime.setJulianDay(mSelectionDay);
3649            startTime.hour = mSelectionHour;
3650            startTime.normalize(true /* ignore isDst */);
3651
3652            Time endTime = new Time(startTime);
3653            endTime.hour++;
3654
3655            mSelectionMode = SELECTION_SELECTED;
3656            mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT,
3657                    CalendarController.EXTRA_GOTO_TIME, null, null);
3658        }
3659        invalidate();
3660    }
3661
3662    private void doLongPress(MotionEvent ev) {
3663        if (mScrolling) {
3664            return;
3665        }
3666
3667        // Scale gesture in progress
3668        if (mStartingSpanY != 0) {
3669            return;
3670        }
3671
3672        int x = (int) ev.getX();
3673        int y = (int) ev.getY();
3674
3675        boolean validPosition = setSelectionFromPosition(x, y);
3676        if (!validPosition) {
3677            // return if the touch wasn't on an area of concern
3678            return;
3679        }
3680
3681        mSelectionMode = SELECTION_LONGPRESS;
3682        invalidate();
3683        performLongClick();
3684    }
3685
3686    private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
3687        cancelAnimation();
3688        if (mStartingScroll) {
3689            mInitialScrollX = 0;
3690            mInitialScrollY = 0;
3691            mStartingScroll = false;
3692        }
3693
3694        mInitialScrollX += deltaX;
3695        mInitialScrollY += deltaY;
3696        int distanceX = (int) mInitialScrollX;
3697        int distanceY = (int) mInitialScrollY;
3698
3699        // If we haven't figured out the predominant scroll direction yet,
3700        // then do it now.
3701        if (mTouchMode == TOUCH_MODE_DOWN) {
3702            int absDistanceX = Math.abs(distanceX);
3703            int absDistanceY = Math.abs(distanceY);
3704            mScrollStartY = mViewStartY;
3705            mPreviousDirection = 0;
3706
3707            if (absDistanceX > absDistanceY) {
3708                mTouchMode = TOUCH_MODE_HSCROLL;
3709                mViewStartX = distanceX;
3710                initNextView(-mViewStartX);
3711            } else {
3712                mTouchMode = TOUCH_MODE_VSCROLL;
3713            }
3714        } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
3715            // We are already scrolling horizontally, so check if we
3716            // changed the direction of scrolling so that the other week
3717            // is now visible.
3718            mViewStartX = distanceX;
3719            if (distanceX != 0) {
3720                int direction = (distanceX > 0) ? 1 : -1;
3721                if (direction != mPreviousDirection) {
3722                    // The user has switched the direction of scrolling
3723                    // so re-init the next view
3724                    initNextView(-mViewStartX);
3725                    mPreviousDirection = direction;
3726                }
3727            }
3728        }
3729
3730        if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
3731            mViewStartY = mScrollStartY + distanceY;
3732
3733            // If dragging while already at the end, do a glow
3734            final int pulledToY = (int) (mScrollStartY + deltaY);
3735            if (pulledToY < 0) {
3736                mEdgeEffectTop.onPull(deltaY / mViewHeight);
3737                if (!mEdgeEffectBottom.isFinished()) {
3738                    mEdgeEffectBottom.onRelease();
3739                }
3740            } else if (pulledToY > mMaxViewStartY) {
3741                mEdgeEffectBottom.onPull(deltaY / mViewHeight);
3742                if (!mEdgeEffectTop.isFinished()) {
3743                    mEdgeEffectTop.onRelease();
3744                }
3745            }
3746
3747            if (mViewStartY < 0) {
3748                mViewStartY = 0;
3749            } else if (mViewStartY > mMaxViewStartY) {
3750                mViewStartY = mMaxViewStartY;
3751            }
3752            computeFirstHour();
3753        }
3754
3755        mScrolling = true;
3756
3757        mSelectionMode = SELECTION_HIDDEN;
3758        invalidate();
3759    }
3760
3761    private void cancelAnimation() {
3762        Animation in = mViewSwitcher.getInAnimation();
3763        if (in != null) {
3764            // cancel() doesn't terminate cleanly.
3765            in.scaleCurrentDuration(0);
3766        }
3767        Animation out = mViewSwitcher.getOutAnimation();
3768        if (out != null) {
3769            // cancel() doesn't terminate cleanly.
3770            out.scaleCurrentDuration(0);
3771        }
3772    }
3773
3774    private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
3775        cancelAnimation();
3776
3777        mSelectionMode = SELECTION_HIDDEN;
3778        mOnFlingCalled = true;
3779
3780        if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
3781            // Horizontal fling.
3782            // initNextView(deltaX);
3783            mTouchMode = TOUCH_MODE_INITIAL_STATE;
3784            if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX);
3785            int deltaX = (int) e2.getX() - (int) e1.getX();
3786            switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX);
3787            mViewStartX = 0;
3788            return;
3789        }
3790
3791        if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) {
3792            if (DEBUG) Log.d(TAG, "doFling: no fling");
3793            return;
3794        }
3795
3796        // Vertical fling.
3797        mTouchMode = TOUCH_MODE_INITIAL_STATE;
3798        mViewStartX = 0;
3799
3800        if (DEBUG) {
3801            Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY);
3802        }
3803
3804        // Continue scrolling vertically
3805        mScrolling = true;
3806        mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */,
3807                (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */,
3808                mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE);
3809
3810        // When flinging down, show a glow when it hits the end only if it
3811        // wasn't started at the top
3812        if (velocityY > 0 && mViewStartY != 0) {
3813            mCallEdgeEffectOnAbsorb = true;
3814        }
3815        // When flinging up, show a glow when it hits the end only if it wasn't
3816        // started at the bottom
3817        else if (velocityY < 0 && mViewStartY != mMaxViewStartY) {
3818            mCallEdgeEffectOnAbsorb = true;
3819        }
3820        post(mContinueScroll);
3821    }
3822
3823    private boolean initNextView(int deltaX) {
3824        // Change the view to the previous day or week
3825        DayView view = (DayView) mViewSwitcher.getNextView();
3826        Time date = view.mBaseDate;
3827        date.set(mBaseDate);
3828        boolean switchForward;
3829        if (deltaX > 0) {
3830            date.monthDay -= mNumDays;
3831            view.mSelectionDay = mSelectionDay - mNumDays;
3832            switchForward = false;
3833        } else {
3834            date.monthDay += mNumDays;
3835            view.mSelectionDay = mSelectionDay + mNumDays;
3836            switchForward = true;
3837        }
3838        date.normalize(true /* ignore isDst */);
3839        initView(view);
3840        view.layout(getLeft(), getTop(), getRight(), getBottom());
3841        view.reloadEvents();
3842        return switchForward;
3843    }
3844
3845    // ScaleGestureDetector.OnScaleGestureListener
3846    public boolean onScaleBegin(ScaleGestureDetector detector) {
3847        mHandleActionUp = false;
3848        float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight;
3849        mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP);
3850
3851        mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY()));
3852        mCellHeightBeforeScaleGesture = mCellHeight;
3853
3854        if (DEBUG_SCALING) {
3855            float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP);
3856            Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour
3857                    + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY
3858                    + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY());
3859        }
3860
3861        return true;
3862    }
3863
3864    // ScaleGestureDetector.OnScaleGestureListener
3865    public boolean onScale(ScaleGestureDetector detector) {
3866        float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY()));
3867
3868        mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY);
3869
3870        if (mCellHeight < mMinCellHeight) {
3871            // If mStartingSpanY is too small, even a small increase in the
3872            // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT
3873            mStartingSpanY = spanY;
3874            mCellHeight = mMinCellHeight;
3875            mCellHeightBeforeScaleGesture = mMinCellHeight;
3876        } else if (mCellHeight > MAX_CELL_HEIGHT) {
3877            mStartingSpanY = spanY;
3878            mCellHeight = MAX_CELL_HEIGHT;
3879            mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT;
3880        }
3881
3882        int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight;
3883        mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels;
3884        mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight;
3885
3886        if (DEBUG_SCALING) {
3887            float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP);
3888            Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: "
3889                    + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:"
3890                    + mCellHeight + " SpanY:" + detector.getCurrentSpanY());
3891        }
3892
3893        if (mViewStartY < 0) {
3894            mViewStartY = 0;
3895            mGestureCenterHour = (mViewStartY + gestureCenterInPixels)
3896                    / (float) (mCellHeight + DAY_GAP);
3897        } else if (mViewStartY > mMaxViewStartY) {
3898            mViewStartY = mMaxViewStartY;
3899            mGestureCenterHour = (mViewStartY + gestureCenterInPixels)
3900                    / (float) (mCellHeight + DAY_GAP);
3901        }
3902        computeFirstHour();
3903
3904        mRemeasure = true;
3905        invalidate();
3906        return true;
3907    }
3908
3909    // ScaleGestureDetector.OnScaleGestureListener
3910    public void onScaleEnd(ScaleGestureDetector detector) {
3911        mScrollStartY = mViewStartY;
3912        mInitialScrollY = 0;
3913        mInitialScrollX = 0;
3914        mStartingSpanY = 0;
3915    }
3916
3917    @Override
3918    public boolean onTouchEvent(MotionEvent ev) {
3919        int action = ev.getAction();
3920        if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount());
3921
3922        if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) {
3923            mScaleGestureDetector.onTouchEvent(ev);
3924            if (mScaleGestureDetector.isInProgress()) {
3925                return true;
3926            }
3927        }
3928
3929        switch (action) {
3930            case MotionEvent.ACTION_DOWN:
3931                mStartingScroll = true;
3932                if (DEBUG) {
3933                    Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt="
3934                            + ev.getPointerCount());
3935                }
3936
3937                int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
3938                if (ev.getY() < bottom) {
3939                    mTouchStartedInAlldayArea = true;
3940                } else {
3941                    mTouchStartedInAlldayArea = false;
3942                }
3943                mHandleActionUp = true;
3944                mGestureDetector.onTouchEvent(ev);
3945                return true;
3946
3947            case MotionEvent.ACTION_MOVE:
3948                if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this);
3949                mGestureDetector.onTouchEvent(ev);
3950                return true;
3951
3952            case MotionEvent.ACTION_UP:
3953                if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp);
3954                mEdgeEffectTop.onRelease();
3955                mEdgeEffectBottom.onRelease();
3956                mStartingScroll = false;
3957                mGestureDetector.onTouchEvent(ev);
3958                if (!mHandleActionUp) {
3959                    mHandleActionUp = true;
3960                    return true;
3961                }
3962                if (mOnFlingCalled) {
3963                    return true;
3964                }
3965                if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
3966                    mTouchMode = TOUCH_MODE_INITIAL_STATE;
3967                    if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) {
3968                        // The user has gone beyond the threshold so switch views
3969                        if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views");
3970                        switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0);
3971                        mViewStartX = 0;
3972                        return true;
3973                    } else {
3974                        // Not beyond the threshold so invalidate which will cause
3975                        // the view to snap back. Also call recalc() to ensure
3976                        // that we have the correct starting date and title.
3977                        if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back");
3978                        recalc();
3979                        invalidate();
3980                        mViewStartX = 0;
3981                    }
3982                }
3983
3984                // If we were scrolling, then reset the selected hour so that it
3985                // is visible.
3986                if (mScrolling) {
3987                    mScrolling = false;
3988                    resetSelectedHour();
3989                    invalidate();
3990                }
3991                return true;
3992
3993                // This case isn't expected to happen.
3994            case MotionEvent.ACTION_CANCEL:
3995                if (DEBUG) Log.e(TAG, "ACTION_CANCEL");
3996                mGestureDetector.onTouchEvent(ev);
3997                mScrolling = false;
3998                resetSelectedHour();
3999                return true;
4000
4001            default:
4002                if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString());
4003                if (mGestureDetector.onTouchEvent(ev)) {
4004                    return true;
4005                }
4006                return super.onTouchEvent(ev);
4007        }
4008    }
4009
4010    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
4011        MenuItem item;
4012
4013        // If the trackball is held down, then the context menu pops up and
4014        // we never get onKeyUp() for the long-press. So check for it here
4015        // and change the selection to the long-press state.
4016        if (mSelectionMode != SELECTION_LONGPRESS) {
4017            mSelectionMode = SELECTION_LONGPRESS;
4018            invalidate();
4019        }
4020
4021        final long startMillis = getSelectedTimeInMillis();
4022        int flags = DateUtils.FORMAT_SHOW_TIME
4023                | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
4024                | DateUtils.FORMAT_SHOW_WEEKDAY;
4025        final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags);
4026        menu.setHeaderTitle(title);
4027
4028        int numSelectedEvents = mSelectedEvents.size();
4029        if (mNumDays == 1) {
4030            // Day view.
4031
4032            // If there is a selected event, then allow it to be viewed and
4033            // edited.
4034            if (numSelectedEvents >= 1) {
4035                item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view);
4036                item.setOnMenuItemClickListener(mContextMenuHandler);
4037                item.setIcon(android.R.drawable.ic_menu_info_details);
4038
4039                int accessLevel = getEventAccessLevel(mContext, mSelectedEvent);
4040                if (accessLevel == ACCESS_LEVEL_EDIT) {
4041                    item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit);
4042                    item.setOnMenuItemClickListener(mContextMenuHandler);
4043                    item.setIcon(android.R.drawable.ic_menu_edit);
4044                    item.setAlphabeticShortcut('e');
4045                }
4046
4047                if (accessLevel >= ACCESS_LEVEL_DELETE) {
4048                    item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete);
4049                    item.setOnMenuItemClickListener(mContextMenuHandler);
4050                    item.setIcon(android.R.drawable.ic_menu_delete);
4051                }
4052
4053                item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4054                item.setOnMenuItemClickListener(mContextMenuHandler);
4055                item.setIcon(android.R.drawable.ic_menu_add);
4056                item.setAlphabeticShortcut('n');
4057            } else {
4058                // Otherwise, if the user long-pressed on a blank hour, allow
4059                // them to create an event. They can also do this by tapping.
4060                item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4061                item.setOnMenuItemClickListener(mContextMenuHandler);
4062                item.setIcon(android.R.drawable.ic_menu_add);
4063                item.setAlphabeticShortcut('n');
4064            }
4065        } else {
4066            // Week view.
4067
4068            // If there is a selected event, then allow it to be viewed and
4069            // edited.
4070            if (numSelectedEvents >= 1) {
4071                item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view);
4072                item.setOnMenuItemClickListener(mContextMenuHandler);
4073                item.setIcon(android.R.drawable.ic_menu_info_details);
4074
4075                int accessLevel = getEventAccessLevel(mContext, mSelectedEvent);
4076                if (accessLevel == ACCESS_LEVEL_EDIT) {
4077                    item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit);
4078                    item.setOnMenuItemClickListener(mContextMenuHandler);
4079                    item.setIcon(android.R.drawable.ic_menu_edit);
4080                    item.setAlphabeticShortcut('e');
4081                }
4082
4083                if (accessLevel >= ACCESS_LEVEL_DELETE) {
4084                    item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete);
4085                    item.setOnMenuItemClickListener(mContextMenuHandler);
4086                    item.setIcon(android.R.drawable.ic_menu_delete);
4087                }
4088            }
4089
4090            item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4091            item.setOnMenuItemClickListener(mContextMenuHandler);
4092            item.setIcon(android.R.drawable.ic_menu_add);
4093            item.setAlphabeticShortcut('n');
4094
4095            item = menu.add(0, MENU_DAY, 0, R.string.show_day_view);
4096            item.setOnMenuItemClickListener(mContextMenuHandler);
4097            item.setIcon(android.R.drawable.ic_menu_day);
4098            item.setAlphabeticShortcut('d');
4099        }
4100
4101        mPopup.dismiss();
4102    }
4103
4104    private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
4105        public boolean onMenuItemClick(MenuItem item) {
4106            switch (item.getItemId()) {
4107                case MENU_EVENT_VIEW: {
4108                    if (mSelectedEvent != null) {
4109                        mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS,
4110                                mSelectedEvent.id, mSelectedEvent.startMillis,
4111                                mSelectedEvent.endMillis, 0, 0, -1);
4112                    }
4113                    break;
4114                }
4115                case MENU_EVENT_EDIT: {
4116                    if (mSelectedEvent != null) {
4117                        mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT,
4118                                mSelectedEvent.id, mSelectedEvent.startMillis,
4119                                mSelectedEvent.endMillis, 0, 0, -1);
4120                    }
4121                    break;
4122                }
4123                case MENU_DAY: {
4124                    mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1,
4125                            ViewType.DAY);
4126                    break;
4127                }
4128                case MENU_AGENDA: {
4129                    mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1,
4130                            ViewType.AGENDA);
4131                    break;
4132                }
4133                case MENU_EVENT_CREATE: {
4134                    long startMillis = getSelectedTimeInMillis();
4135                    long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
4136                    mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1,
4137                            startMillis, endMillis, 0, 0, -1);
4138                    break;
4139                }
4140                case MENU_EVENT_DELETE: {
4141                    if (mSelectedEvent != null) {
4142                        Event selectedEvent = mSelectedEvent;
4143                        long begin = selectedEvent.startMillis;
4144                        long end = selectedEvent.endMillis;
4145                        long id = selectedEvent.id;
4146                        mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin,
4147                                end, 0, 0, -1);
4148                    }
4149                    break;
4150                }
4151                default: {
4152                    return false;
4153                }
4154            }
4155            return true;
4156        }
4157    }
4158
4159    private static int getEventAccessLevel(Context context, Event e) {
4160        ContentResolver cr = context.getContentResolver();
4161
4162        int accessLevel = Calendars.CAL_ACCESS_NONE;
4163
4164        // Get the calendar id for this event
4165        Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
4166                new String[] { Events.CALENDAR_ID },
4167                null /* selection */,
4168                null /* selectionArgs */,
4169                null /* sort */);
4170
4171        if (cursor == null) {
4172            return ACCESS_LEVEL_NONE;
4173        }
4174
4175        if (cursor.getCount() == 0) {
4176            cursor.close();
4177            return ACCESS_LEVEL_NONE;
4178        }
4179
4180        cursor.moveToFirst();
4181        long calId = cursor.getLong(0);
4182        cursor.close();
4183
4184        Uri uri = Calendars.CONTENT_URI;
4185        String where = String.format(CALENDARS_WHERE, calId);
4186        cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
4187
4188        String calendarOwnerAccount = null;
4189        if (cursor != null) {
4190            cursor.moveToFirst();
4191            accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
4192            calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
4193            cursor.close();
4194        }
4195
4196        if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) {
4197            return ACCESS_LEVEL_NONE;
4198        }
4199
4200        if (e.guestsCanModify) {
4201            return ACCESS_LEVEL_EDIT;
4202        }
4203
4204        if (!TextUtils.isEmpty(calendarOwnerAccount)
4205                && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) {
4206            return ACCESS_LEVEL_EDIT;
4207        }
4208
4209        return ACCESS_LEVEL_DELETE;
4210    }
4211
4212    /**
4213     * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
4214     * If the touch position is not within the displayed grid, then this
4215     * method returns false.
4216     *
4217     * @param x the x position of the touch
4218     * @param y the y position of the touch
4219     * @return true if the touch position is valid
4220     */
4221    private boolean setSelectionFromPosition(int x, final int y) {
4222        if (x < mHoursWidth) {
4223            x = mHoursWidth;
4224        }
4225
4226        int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
4227        if (day >= mNumDays) {
4228            day = mNumDays - 1;
4229        }
4230        day += mFirstJulianDay;
4231        mSelectionDay = day;
4232
4233        if (y < DAY_HEADER_HEIGHT) {
4234            sendAccessibilityEventAsNeeded(false);
4235            return false;
4236        }
4237
4238        mSelectionHour = mFirstHour; /* First fully visible hour */
4239
4240        if (y < mFirstCell) {
4241            mSelectionAllday = true;
4242        } else {
4243            // y is now offset from top of the scrollable region
4244            int adjustedY = y - mFirstCell;
4245
4246            if (adjustedY < mFirstHourOffset) {
4247                --mSelectionHour; /* In the partially visible hour */
4248            } else {
4249                mSelectionHour += (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
4250            }
4251
4252            mSelectionAllday = false;
4253        }
4254
4255        findSelectedEvent(x, y);
4256
4257//        Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: "
4258//                + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: "
4259//                + mFirstHourOffset);
4260//        if (mSelectedEvent != null) {
4261//            Log.i("Cal", "  num events: " + mSelectedEvents.size() + " event: "
4262//                    + mSelectedEvent.title);
4263//            for (Event ev : mSelectedEvents) {
4264//                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
4265//                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
4266//                String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags);
4267//
4268//                Log.i("Cal", "  " + timeRange + " " + ev.title);
4269//            }
4270//        }
4271        sendAccessibilityEventAsNeeded(true);
4272        return true;
4273    }
4274
4275    private void findSelectedEvent(int x, int y) {
4276        int date = mSelectionDay;
4277        int cellWidth = mCellWidth;
4278        ArrayList<Event> events = mEvents;
4279        int numEvents = events.size();
4280        int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay);
4281        int top = 0;
4282        mSelectedEvent = null;
4283
4284        mSelectedEvents.clear();
4285        if (mSelectionAllday) {
4286            float yDistance;
4287            float minYdistance = 10000.0f; // any large number
4288            Event closestEvent = null;
4289            float drawHeight = mAlldayHeight;
4290            int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
4291            int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount;
4292            if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
4293                // Leave a gap for the 'box +n' text
4294                maxUnexpandedColumn--;
4295            }
4296            events = mAllDayEvents;
4297            numEvents = events.size();
4298            for (int i = 0; i < numEvents; i++) {
4299                Event event = events.get(i);
4300                if (!event.drawAsAllday() ||
4301                        (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) {
4302                    // Don't check non-allday events or events that aren't shown
4303                    continue;
4304                }
4305
4306                if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
4307                    float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents
4308                            : mMaxUnexpandedAlldayEventCount;
4309                    float height = drawHeight / numRectangles;
4310                    if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
4311                        height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
4312                    }
4313                    float eventTop = yOffset + height * event.getColumn();
4314                    float eventBottom = eventTop + height;
4315                    if (eventTop < y && eventBottom > y) {
4316                        // If the touch is inside the event rectangle, then
4317                        // add the event.
4318                        mSelectedEvents.add(event);
4319                        closestEvent = event;
4320                        break;
4321                    } else {
4322                        // Find the closest event
4323                        if (eventTop >= y) {
4324                            yDistance = eventTop - y;
4325                        } else {
4326                            yDistance = y - eventBottom;
4327                        }
4328                        if (yDistance < minYdistance) {
4329                            minYdistance = yDistance;
4330                            closestEvent = event;
4331                        }
4332                    }
4333                }
4334            }
4335            mSelectedEvent = closestEvent;
4336            return;
4337        }
4338
4339        // Adjust y for the scrollable bitmap
4340        y += mViewStartY - mFirstCell;
4341
4342        // Use a region around (x,y) for the selection region
4343        Rect region = mRect;
4344        region.left = x - 10;
4345        region.right = x + 10;
4346        region.top = y - 10;
4347        region.bottom = y + 10;
4348
4349        EventGeometry geometry = mEventGeometry;
4350
4351        for (int i = 0; i < numEvents; i++) {
4352            Event event = events.get(i);
4353            // Compute the event rectangle.
4354            if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
4355                continue;
4356            }
4357
4358            // If the event intersects the selection region, then add it to
4359            // mSelectedEvents.
4360            if (geometry.eventIntersectsSelection(event, region)) {
4361                mSelectedEvents.add(event);
4362            }
4363        }
4364
4365        // If there are any events in the selected region, then assign the
4366        // closest one to mSelectedEvent.
4367        if (mSelectedEvents.size() > 0) {
4368            int len = mSelectedEvents.size();
4369            Event closestEvent = null;
4370            float minDist = mViewWidth + mViewHeight; // some large distance
4371            for (int index = 0; index < len; index++) {
4372                Event ev = mSelectedEvents.get(index);
4373                float dist = geometry.pointToEvent(x, y, ev);
4374                if (dist < minDist) {
4375                    minDist = dist;
4376                    closestEvent = ev;
4377                }
4378            }
4379            mSelectedEvent = closestEvent;
4380
4381            // Keep the selected hour and day consistent with the selected
4382            // event. They could be different if we touched on an empty hour
4383            // slot very close to an event in the previous hour slot. In
4384            // that case we will select the nearby event.
4385            int startDay = mSelectedEvent.startDay;
4386            int endDay = mSelectedEvent.endDay;
4387            if (mSelectionDay < startDay) {
4388                mSelectionDay = startDay;
4389            } else if (mSelectionDay > endDay) {
4390                mSelectionDay = endDay;
4391            }
4392
4393            int startHour = mSelectedEvent.startTime / 60;
4394            int endHour;
4395            if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
4396                endHour = (mSelectedEvent.endTime - 1) / 60;
4397            } else {
4398                endHour = mSelectedEvent.endTime / 60;
4399            }
4400
4401            if (mSelectionHour < startHour && mSelectionDay == startDay) {
4402                mSelectionHour = startHour;
4403            } else if (mSelectionHour > endHour && mSelectionDay == endDay) {
4404                mSelectionHour = endHour;
4405            }
4406        }
4407    }
4408
4409    // Encapsulates the code to continue the scrolling after the
4410    // finger is lifted. Instead of stopping the scroll immediately,
4411    // the scroll continues to "free spin" and gradually slows down.
4412    private class ContinueScroll implements Runnable {
4413        public void run() {
4414            mScrolling = mScrolling && mScroller.computeScrollOffset();
4415            if (!mScrolling) {
4416                resetSelectedHour();
4417                invalidate();
4418                return;
4419            }
4420
4421            mViewStartY = mScroller.getCurrY();
4422
4423            if (mCallEdgeEffectOnAbsorb) {
4424                if (mViewStartY < 0) {
4425                    mEdgeEffectTop.onAbsorb((int) mLastVelocity);
4426                    mCallEdgeEffectOnAbsorb = false;
4427                } else if (mViewStartY > mMaxViewStartY) {
4428                    mEdgeEffectBottom.onAbsorb((int) mLastVelocity);
4429                    mCallEdgeEffectOnAbsorb = false;
4430                }
4431                mLastVelocity = mScroller.getCurrVelocity();
4432            }
4433
4434            if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) {
4435                // Allow overscroll/springback only on a fling,
4436                // not a pull/fling from the end
4437                if (mViewStartY < 0) {
4438                    mViewStartY = 0;
4439                } else if (mViewStartY > mMaxViewStartY) {
4440                    mViewStartY = mMaxViewStartY;
4441                }
4442            }
4443
4444            computeFirstHour();
4445            post(this);
4446            invalidate();
4447        }
4448    }
4449
4450    /**
4451     * Cleanup the pop-up and timers.
4452     */
4453    public void cleanup() {
4454        // Protect against null-pointer exceptions
4455        if (mPopup != null) {
4456            mPopup.dismiss();
4457        }
4458        mLastPopupEventID = INVALID_EVENT_ID;
4459        Handler handler = getHandler();
4460        if (handler != null) {
4461            handler.removeCallbacks(mDismissPopup);
4462            handler.removeCallbacks(mUpdateCurrentTime);
4463        }
4464
4465        Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT,
4466            mCellHeight);
4467
4468        // Turn off redraw
4469        mRemeasure = false;
4470    }
4471
4472    /**
4473     * Restart the update timer
4474     */
4475    public void restartCurrentTimeUpdates() {
4476        post(mUpdateCurrentTime);
4477    }
4478
4479    @Override
4480    protected void onDetachedFromWindow() {
4481        cleanup();
4482        super.onDetachedFromWindow();
4483    }
4484
4485    class DismissPopup implements Runnable {
4486        public void run() {
4487            // Protect against null-pointer exceptions
4488            if (mPopup != null) {
4489                mPopup.dismiss();
4490            }
4491        }
4492    }
4493
4494    class UpdateCurrentTime implements Runnable {
4495        public void run() {
4496            long currentTime = System.currentTimeMillis();
4497            mCurrentTime.set(currentTime);
4498            //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
4499            postDelayed(mUpdateCurrentTime,
4500                    UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
4501            mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
4502            invalidate();
4503        }
4504    }
4505
4506    class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
4507        @Override
4508        public boolean onSingleTapUp(MotionEvent ev) {
4509            if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp");
4510            DayView.this.doSingleTapUp(ev);
4511            return true;
4512        }
4513
4514        @Override
4515        public void onLongPress(MotionEvent ev) {
4516            if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress");
4517            DayView.this.doLongPress(ev);
4518        }
4519
4520        @Override
4521        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
4522            if (DEBUG) Log.e(TAG, "GestureDetector.onScroll");
4523            if (mTouchStartedInAlldayArea) {
4524                if (Math.abs(distanceX) < Math.abs(distanceY)) {
4525                    return false;
4526                }
4527                // don't scroll vertically if this started in the allday area
4528                distanceY = 0;
4529            }
4530            DayView.this.doScroll(e1, e2, distanceX, distanceY);
4531            return true;
4532        }
4533
4534        @Override
4535        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
4536            if (DEBUG) Log.e(TAG, "GestureDetector.onFling");
4537
4538            if (mTouchStartedInAlldayArea) {
4539                if (Math.abs(velocityX) < Math.abs(velocityY)) {
4540                    return false;
4541                }
4542                // don't fling vertically if this started in the allday area
4543                velocityY = 0;
4544            }
4545            DayView.this.doFling(e1, e2, velocityX, velocityY);
4546            return true;
4547        }
4548
4549        @Override
4550        public boolean onDown(MotionEvent ev) {
4551            if (DEBUG) Log.e(TAG, "GestureDetector.onDown");
4552            DayView.this.doDown(ev);
4553            return true;
4554        }
4555    }
4556
4557    @Override
4558    public boolean onLongClick(View v) {
4559        int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
4560        long time = getSelectedTimeInMillis();
4561        if (!mSelectionAllday) {
4562            flags |= DateUtils.FORMAT_SHOW_TIME;
4563        }
4564        if (DateFormat.is24HourFormat(mContext)) {
4565            flags |= DateUtils.FORMAT_24HOUR;
4566        }
4567        mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags);
4568        new AlertDialog.Builder(mContext).setTitle(mLongPressTitle)
4569                .setItems(mLongPressItems, new DialogInterface.OnClickListener() {
4570                    @Override
4571                    public void onClick(DialogInterface dialog, int which) {
4572                        if (which == 0) {
4573                            long extraLong = 0;
4574                            if (mSelectionAllday) {
4575                                extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
4576                            }
4577                            mController.sendEventRelatedEventWithExtra(this,
4578                                    EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1,
4579                                    -1, extraLong, -1);
4580                        }
4581                    }
4582                }).show().setCanceledOnTouchOutside(true);
4583        return true;
4584    }
4585
4586    // The rest of this file was borrowed from Launcher2 - PagedView.java
4587    private static final int MINIMUM_SNAP_VELOCITY = 2200;
4588
4589    private class ScrollInterpolator implements Interpolator {
4590        public ScrollInterpolator() {
4591        }
4592
4593        public float getInterpolation(float t) {
4594            t -= 1.0f;
4595            t = t * t * t * t * t + 1;
4596
4597            if ((1 - t) * mAnimationDistance < 1) {
4598                cancelAnimation();
4599            }
4600
4601            return t;
4602        }
4603    }
4604
4605    private long calculateDuration(float delta, float width, float velocity) {
4606        /*
4607         * Here we compute a "distance" that will be used in the computation of
4608         * the overall snap duration. This is a function of the actual distance
4609         * that needs to be traveled; we keep this value close to half screen
4610         * size in order to reduce the variance in snap duration as a function
4611         * of the distance the page needs to travel.
4612         */
4613        final float halfScreenSize = width / 2;
4614        float distanceRatio = delta / width;
4615        float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio);
4616        float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration;
4617
4618        velocity = Math.abs(velocity);
4619        velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity);
4620
4621        /*
4622         * we want the page's snap velocity to approximately match the velocity
4623         * at which the user flings, so we scale the duration by a value near to
4624         * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to
4625         * make it a little slower.
4626         */
4627        long duration = 6 * Math.round(1000 * Math.abs(distance / velocity));
4628        if (DEBUG) {
4629            Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:"
4630                    + distanceRatio + " distance:" + distance + " velocity:" + velocity
4631                    + " duration:" + duration + " distanceInfluenceForSnapDuration:"
4632                    + distanceInfluenceForSnapDuration);
4633        }
4634        return duration;
4635    }
4636
4637    /*
4638     * We want the duration of the page snap animation to be influenced by the
4639     * distance that the screen has to travel, however, we don't want this
4640     * duration to be effected in a purely linear fashion. Instead, we use this
4641     * method to moderate the effect that the distance of travel has on the
4642     * overall snap duration.
4643     */
4644    private float distanceInfluenceForSnapDuration(float f) {
4645        f -= 0.5f; // center the values about 0.
4646        f *= 0.3f * Math.PI / 2.0f;
4647        return (float) Math.sin(f);
4648    }
4649}
4650