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