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