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