DayView.java revision 4dcabc96044eb83be2320e753fee9b1dc266ec32
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 (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1958                    && mFirstHour > 0 && mFirstHour < 8) {
1959                mPrevSelectedEvent = null;
1960                mSelectionAllday = true;
1961                setSelectedHour(mFirstHour + 1);
1962                return;
1963            }
1964
1965            if (mFirstHour > 0) {
1966                mFirstHour -= 1;
1967                mViewStartY -= (mCellHeight + HOUR_GAP);
1968                if (mViewStartY < 0) {
1969                    mViewStartY = 0;
1970                }
1971                return;
1972            }
1973        }
1974
1975        if (mSelectionHour > mFirstHour + mNumHours - 3) {
1976            if (mFirstHour < 24 - mNumHours) {
1977                mFirstHour += 1;
1978                mViewStartY += (mCellHeight + HOUR_GAP);
1979                if (mViewStartY > mMaxViewStartY) {
1980                    mViewStartY = mMaxViewStartY;
1981                }
1982                return;
1983            } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1984                mViewStartY = mMaxViewStartY;
1985            }
1986        }
1987    }
1988
1989    void clearCachedEvents() {
1990        mLastReloadMillis = 0;
1991    }
1992
1993    private final Runnable mCancelCallback = new Runnable() {
1994        public void run() {
1995            clearCachedEvents();
1996        }
1997    };
1998
1999    /* package */ void reloadEvents() {
2000        // Protect against this being called before this view has been
2001        // initialized.
2002//        if (mContext == null) {
2003//            return;
2004//        }
2005
2006        // Make sure our time zones are up to date
2007        mTZUpdater.run();
2008
2009        setSelectedEvent(null);
2010        mPrevSelectedEvent = null;
2011        mSelectedEvents.clear();
2012
2013        // The start date is the beginning of the week at 12am
2014        Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater));
2015        weekStart.set(mBaseDate);
2016        weekStart.hour = 0;
2017        weekStart.minute = 0;
2018        weekStart.second = 0;
2019        long millis = weekStart.normalize(true /* ignore isDst */);
2020
2021        // Avoid reloading events unnecessarily.
2022        if (millis == mLastReloadMillis) {
2023            return;
2024        }
2025        mLastReloadMillis = millis;
2026
2027        // load events in the background
2028//        mContext.startProgressSpinner();
2029        final ArrayList<Event> events = new ArrayList<Event>();
2030        mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() {
2031            public void run() {
2032                boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay;
2033                mEvents = events;
2034                mLoadedFirstJulianDay = mFirstJulianDay;
2035                if (mAllDayEvents == null) {
2036                    mAllDayEvents = new ArrayList<Event>();
2037                } else {
2038                    mAllDayEvents.clear();
2039                }
2040
2041                // Create a shorter array for all day events
2042                for (Event e : events) {
2043                    if (e.drawAsAllday()) {
2044                        mAllDayEvents.add(e);
2045                    }
2046                }
2047
2048                // New events, new layouts
2049                if (mLayouts == null || mLayouts.length < events.size()) {
2050                    mLayouts = new StaticLayout[events.size()];
2051                } else {
2052                    Arrays.fill(mLayouts, null);
2053                }
2054
2055                if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) {
2056                    mAllDayLayouts = new StaticLayout[events.size()];
2057                } else {
2058                    Arrays.fill(mAllDayLayouts, null);
2059                }
2060
2061                computeEventRelations();
2062
2063                mRemeasure = true;
2064                mComputeSelectedEvents = true;
2065                recalc();
2066
2067                // Start animation to cross fade the events
2068                if (fadeinEvents) {
2069                    if (mEventsCrossFadeAnimation == null) {
2070                        mEventsCrossFadeAnimation =
2071                                ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255);
2072                        mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION);
2073                    }
2074                    mEventsCrossFadeAnimation.start();
2075                } else{
2076                    invalidate();
2077                }
2078            }
2079        }, mCancelCallback);
2080    }
2081
2082    public void setEventsAlpha(int alpha) {
2083        mEventsAlpha = alpha;
2084        invalidate();
2085    }
2086
2087    public int getEventsAlpha() {
2088        return mEventsAlpha;
2089    }
2090
2091    public void stopEventsAnimation() {
2092        if (mEventsCrossFadeAnimation != null) {
2093            mEventsCrossFadeAnimation.cancel();
2094        }
2095        mEventsAlpha = 255;
2096    }
2097
2098    private void computeEventRelations() {
2099        // Compute the layout relation between each event before measuring cell
2100        // width, as the cell width should be adjusted along with the relation.
2101        //
2102        // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm)
2103        // We should mark them as "overwapped". Though they are not overwapped logically, but
2104        // minimum cell height implicitly expands the cell height of A and it should look like
2105        // (1:00pm - 1:15pm) after the cell height adjustment.
2106
2107        // Compute the space needed for the all-day events, if any.
2108        // Make a pass over all the events, and keep track of the maximum
2109        // number of all-day events in any one day.  Also, keep track of
2110        // the earliest event in each day.
2111        int maxAllDayEvents = 0;
2112        final ArrayList<Event> events = mEvents;
2113        final int len = events.size();
2114        // Num of all-day-events on each day.
2115        final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1];
2116        Arrays.fill(eventsCount, 0);
2117        for (int ii = 0; ii < len; ii++) {
2118            Event event = events.get(ii);
2119            if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) {
2120                continue;
2121            }
2122            if (event.drawAsAllday()) {
2123                // Count all the events being drawn as allDay events
2124                final int firstDay = Math.max(event.startDay, mFirstJulianDay);
2125                final int lastDay = Math.min(event.endDay, mLastJulianDay);
2126                for (int day = firstDay; day <= lastDay; day++) {
2127                    final int count = ++eventsCount[day - mFirstJulianDay];
2128                    if (maxAllDayEvents < count) {
2129                        maxAllDayEvents = count;
2130                    }
2131                }
2132
2133                int daynum = event.startDay - mFirstJulianDay;
2134                int durationDays = event.endDay - event.startDay + 1;
2135                if (daynum < 0) {
2136                    durationDays += daynum;
2137                    daynum = 0;
2138                }
2139                if (daynum + durationDays > mNumDays) {
2140                    durationDays = mNumDays - daynum;
2141                }
2142                for (int day = daynum; durationDays > 0; day++, durationDays--) {
2143                    mHasAllDayEvent[day] = true;
2144                }
2145            } else {
2146                int daynum = event.startDay - mFirstJulianDay;
2147                int hour = event.startTime / 60;
2148                if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
2149                    mEarliestStartHour[daynum] = hour;
2150                }
2151
2152                // Also check the end hour in case the event spans more than
2153                // one day.
2154                daynum = event.endDay - mFirstJulianDay;
2155                hour = event.endTime / 60;
2156                if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
2157                    mEarliestStartHour[daynum] = hour;
2158                }
2159            }
2160        }
2161        mMaxAlldayEvents = maxAllDayEvents;
2162        initAllDayHeights();
2163    }
2164
2165    @Override
2166    protected void onDraw(Canvas canvas) {
2167        if (mRemeasure) {
2168            remeasure(getWidth(), getHeight());
2169            mRemeasure = false;
2170        }
2171        canvas.save();
2172
2173        float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight;
2174        // offset canvas by the current drag and header position
2175        canvas.translate(-mViewStartX, yTranslate);
2176        // clip to everything below the allDay area
2177        Rect dest = mDestRect;
2178        dest.top = (int) (mFirstCell - yTranslate);
2179        dest.bottom = (int) (mViewHeight - yTranslate);
2180        dest.left = 0;
2181        dest.right = mViewWidth;
2182        canvas.save();
2183        canvas.clipRect(dest);
2184        // Draw the movable part of the view
2185        doDraw(canvas);
2186        // restore to having no clip
2187        canvas.restore();
2188
2189        if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2190            float xTranslate;
2191            if (mViewStartX > 0) {
2192                xTranslate = mViewWidth;
2193            } else {
2194                xTranslate = -mViewWidth;
2195            }
2196            // Move the canvas around to prep it for the next view
2197            // specifically, shift it by a screen and undo the
2198            // yTranslation which will be redone in the nextView's onDraw().
2199            canvas.translate(xTranslate, -yTranslate);
2200            DayView nextView = (DayView) mViewSwitcher.getNextView();
2201
2202            // Prevent infinite recursive calls to onDraw().
2203            nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
2204
2205            nextView.onDraw(canvas);
2206            // Move it back for this view
2207            canvas.translate(-xTranslate, 0);
2208        } else {
2209            // If we drew another view we already translated it back
2210            // If we didn't draw another view we should be at the edge of the
2211            // screen
2212            canvas.translate(mViewStartX, -yTranslate);
2213        }
2214
2215        // Draw the fixed areas (that don't scroll) directly to the canvas.
2216        drawAfterScroll(canvas);
2217        if (mComputeSelectedEvents && mUpdateToast) {
2218            updateEventDetails();
2219            mUpdateToast = false;
2220        }
2221        mComputeSelectedEvents = false;
2222
2223        // Draw overscroll glow
2224        if (!mEdgeEffectTop.isFinished()) {
2225            if (DAY_HEADER_HEIGHT != 0) {
2226                canvas.translate(0, DAY_HEADER_HEIGHT);
2227            }
2228            if (mEdgeEffectTop.draw(canvas)) {
2229                invalidate();
2230            }
2231            if (DAY_HEADER_HEIGHT != 0) {
2232                canvas.translate(0, -DAY_HEADER_HEIGHT);
2233            }
2234        }
2235        if (!mEdgeEffectBottom.isFinished()) {
2236            canvas.rotate(180, mViewWidth/2, mViewHeight/2);
2237            if (mEdgeEffectBottom.draw(canvas)) {
2238                invalidate();
2239            }
2240        }
2241        canvas.restore();
2242    }
2243
2244    private void drawAfterScroll(Canvas canvas) {
2245        Paint p = mPaint;
2246        Rect r = mRect;
2247
2248        drawAllDayHighlights(r, canvas, p);
2249        if (mMaxAlldayEvents != 0) {
2250            drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p);
2251            drawUpperLeftCorner(r, canvas, p);
2252        }
2253
2254        drawScrollLine(r, canvas, p);
2255        drawDayHeaderLoop(r, canvas, p);
2256
2257        // Draw the AM and PM indicators if we're in 12 hour mode
2258        if (!mIs24HourFormat) {
2259            drawAmPm(canvas, p);
2260        }
2261    }
2262
2263    // This isn't really the upper-left corner. It's the square area just
2264    // below the upper-left corner, above the hours and to the left of the
2265    // all-day area.
2266    private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
2267        setupHourTextPaint(p);
2268        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2269            // Draw the allDay expand/collapse icon
2270            if (mUseExpandIcon) {
2271                mExpandAlldayDrawable.setBounds(mExpandAllDayRect);
2272                mExpandAlldayDrawable.draw(canvas);
2273            } else {
2274                mCollapseAlldayDrawable.setBounds(mExpandAllDayRect);
2275                mCollapseAlldayDrawable.draw(canvas);
2276            }
2277        }
2278    }
2279
2280    private void drawScrollLine(Rect r, Canvas canvas, Paint p) {
2281        final int right = computeDayLeftPosition(mNumDays);
2282        final int y = mFirstCell - 1;
2283
2284        p.setAntiAlias(false);
2285        p.setStyle(Style.FILL);
2286
2287        p.setColor(mCalendarGridLineInnerHorizontalColor);
2288        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2289        canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p);
2290        p.setAntiAlias(true);
2291    }
2292
2293    // Computes the x position for the left side of the given day (base 0)
2294    private int computeDayLeftPosition(int day) {
2295        int effectiveWidth = mViewWidth - mHoursWidth;
2296        return day * effectiveWidth / mNumDays + mHoursWidth;
2297    }
2298
2299    private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) {
2300        if (mFutureBgColor != 0) {
2301            // First, color the labels area light gray
2302            r.top = 0;
2303            r.bottom = DAY_HEADER_HEIGHT;
2304            r.left = 0;
2305            r.right = mViewWidth;
2306            p.setColor(mBgColor);
2307            p.setStyle(Style.FILL);
2308            canvas.drawRect(r, p);
2309            // and the area that says All day
2310            r.top = DAY_HEADER_HEIGHT;
2311            r.bottom = mFirstCell - 1;
2312            r.left = 0;
2313            r.right = mHoursWidth;
2314            canvas.drawRect(r, p);
2315
2316            int startIndex = -1;
2317
2318            int todayIndex = mTodayJulianDay - mFirstJulianDay;
2319            if (todayIndex < 0) {
2320                // Future
2321                startIndex = 0;
2322            } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) {
2323                // Multiday - tomorrow is visible.
2324                startIndex = todayIndex + 1;
2325            }
2326
2327            if (startIndex >= 0) {
2328                // Draw the future highlight
2329                r.top = 0;
2330                r.bottom = mFirstCell - 1;
2331                r.left = computeDayLeftPosition(startIndex) + 1;
2332                r.right = computeDayLeftPosition(mNumDays);
2333                p.setColor(mFutureBgColor);
2334                p.setStyle(Style.FILL);
2335                canvas.drawRect(r, p);
2336            }
2337        }
2338
2339        if (mSelectionAllday && mSelectionMode != SELECTION_HIDDEN) {
2340            // Draw the selection highlight on the selected all-day area
2341            mRect.top = DAY_HEADER_HEIGHT + 1;
2342            mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2;
2343            int daynum = mSelectionDay - mFirstJulianDay;
2344            mRect.left = computeDayLeftPosition(daynum) + 1;
2345            mRect.right = computeDayLeftPosition(daynum + 1);
2346            p.setColor(mCalendarGridAreaSelected);
2347            canvas.drawRect(mRect, p);
2348        }
2349    }
2350
2351    private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
2352        // Draw the horizontal day background banner
2353        // p.setColor(mCalendarDateBannerBackground);
2354        // r.top = 0;
2355        // r.bottom = DAY_HEADER_HEIGHT;
2356        // r.left = 0;
2357        // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
2358        // canvas.drawRect(r, p);
2359        //
2360        // Fill the extra space on the right side with the default background
2361        // r.left = r.right;
2362        // r.right = mViewWidth;
2363        // p.setColor(mCalendarGridAreaBackground);
2364        // canvas.drawRect(r, p);
2365        if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) {
2366            return;
2367        }
2368
2369        p.setTypeface(mBold);
2370        p.setTextAlign(Paint.Align.RIGHT);
2371        int cell = mFirstJulianDay;
2372
2373        String[] dayNames;
2374        if (mDateStrWidth < mCellWidth) {
2375            dayNames = mDayStrs;
2376        } else {
2377            dayNames = mDayStrs2Letter;
2378        }
2379
2380        p.setAntiAlias(true);
2381        for (int day = 0; day < mNumDays; day++, cell++) {
2382            int dayOfWeek = day + mFirstVisibleDayOfWeek;
2383            if (dayOfWeek >= 14) {
2384                dayOfWeek -= 14;
2385            }
2386
2387            int color = mCalendarDateBannerTextColor;
2388            if (mNumDays == 1) {
2389                if (dayOfWeek == Time.SATURDAY) {
2390                    color = mWeek_saturdayColor;
2391                } else if (dayOfWeek == Time.SUNDAY) {
2392                    color = mWeek_sundayColor;
2393                }
2394            } else {
2395                final int column = day % 7;
2396                if (Utils.isSaturday(column, mFirstDayOfWeek)) {
2397                    color = mWeek_saturdayColor;
2398                } else if (Utils.isSunday(column, mFirstDayOfWeek)) {
2399                    color = mWeek_sundayColor;
2400                }
2401            }
2402
2403            p.setColor(color);
2404            drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p);
2405        }
2406        p.setTypeface(null);
2407    }
2408
2409    private void drawAmPm(Canvas canvas, Paint p) {
2410        p.setColor(mCalendarAmPmLabel);
2411        p.setTextSize(AMPM_TEXT_SIZE);
2412        p.setTypeface(mBold);
2413        p.setAntiAlias(true);
2414        p.setTextAlign(Paint.Align.RIGHT);
2415        String text = mAmString;
2416        if (mFirstHour >= 12) {
2417            text = mPmString;
2418        }
2419        int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
2420        canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2421
2422        if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
2423            // Also draw the "PM"
2424            text = mPmString;
2425            y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
2426                    + 2 * mHoursTextHeight + HOUR_GAP;
2427            canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2428        }
2429    }
2430
2431    private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas,
2432            Paint p) {
2433        r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1;
2434        r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1;
2435
2436        r.top = top - CURRENT_TIME_LINE_TOP_OFFSET;
2437        r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight();
2438
2439        mCurrentTimeLine.setBounds(r);
2440        mCurrentTimeLine.draw(canvas);
2441        if (mAnimateToday) {
2442            mCurrentTimeAnimateLine.setBounds(r);
2443            mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha);
2444            mCurrentTimeAnimateLine.draw(canvas);
2445        }
2446    }
2447
2448    private void doDraw(Canvas canvas) {
2449        Paint p = mPaint;
2450        Rect r = mRect;
2451
2452        if (mFutureBgColor != 0) {
2453            drawBgColors(r, canvas, p);
2454        }
2455        drawGridBackground(r, canvas, p);
2456        drawHours(r, canvas, p);
2457
2458        // Draw each day
2459        int cell = mFirstJulianDay;
2460        p.setAntiAlias(false);
2461        int alpha = p.getAlpha();
2462        p.setAlpha(mEventsAlpha);
2463        for (int day = 0; day < mNumDays; day++, cell++) {
2464            // TODO Wow, this needs cleanup. drawEvents loop through all the
2465            // events on every call.
2466            drawEvents(cell, day, HOUR_GAP, canvas, p);
2467            // If this is today
2468            if (cell == mTodayJulianDay) {
2469                int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2470                        + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2471
2472                // And the current time shows up somewhere on the screen
2473                if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
2474                    drawCurrentTimeLine(r, day, lineY, canvas, p);
2475                }
2476            }
2477        }
2478        p.setAntiAlias(true);
2479        p.setAlpha(alpha);
2480
2481        drawSelectedRect(r, canvas, p);
2482    }
2483
2484    private void drawSelectedRect(Rect r, Canvas canvas, Paint p) {
2485        // Draw a highlight on the selected hour (if needed)
2486        if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) {
2487            int daynum = mSelectionDay - mFirstJulianDay;
2488            r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2489            r.bottom = r.top + mCellHeight + HOUR_GAP;
2490            r.left = computeDayLeftPosition(daynum) + 1;
2491            r.right = computeDayLeftPosition(daynum + 1) + 1;
2492
2493            saveSelectionPosition(r.left, r.top, r.right, r.bottom);
2494
2495            // Draw the highlight on the grid
2496            p.setColor(mCalendarGridAreaSelected);
2497            r.top += HOUR_GAP;
2498            r.right -= DAY_GAP;
2499            p.setAntiAlias(false);
2500            canvas.drawRect(r, p);
2501
2502            // Draw a "new event hint" on top of the highlight
2503            // For the week view, show a "+", for day view, show "+ New event"
2504            p.setColor(mNewEventHintColor);
2505            if (mNumDays > 1) {
2506                p.setStrokeWidth(NEW_EVENT_WIDTH);
2507                int width = r.right - r.left;
2508                int midX = r.left + width / 2;
2509                int midY = r.top + mCellHeight / 2;
2510                int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2;
2511                length = Math.min(length, NEW_EVENT_MAX_LENGTH);
2512                int verticalPadding = (mCellHeight - length) / 2;
2513                int horizontalPadding = (width - length) / 2;
2514                canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding,
2515                        midY, p);
2516                canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p);
2517            } else {
2518                p.setStyle(Paint.Style.FILL);
2519                p.setTextSize(NEW_EVENT_HINT_FONT_SIZE);
2520                p.setTextAlign(Paint.Align.LEFT);
2521                p.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
2522                canvas.drawText(mNewEventHintString, r.left + EVENT_TEXT_LEFT_MARGIN,
2523                        r.top + Math.abs(p.getFontMetrics().ascent) + EVENT_TEXT_TOP_MARGIN , p);
2524            }
2525        }
2526    }
2527
2528    private void drawHours(Rect r, Canvas canvas, Paint p) {
2529        setupHourTextPaint(p);
2530
2531        int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN;
2532
2533        for (int i = 0; i < 24; i++) {
2534            String time = mHourStrs[i];
2535            canvas.drawText(time, HOURS_LEFT_MARGIN, y, p);
2536            y += mCellHeight + HOUR_GAP;
2537        }
2538    }
2539
2540    private void setupHourTextPaint(Paint p) {
2541        p.setColor(mCalendarHourLabelColor);
2542        p.setTextSize(HOURS_TEXT_SIZE);
2543        p.setTypeface(Typeface.DEFAULT);
2544        p.setTextAlign(Paint.Align.RIGHT);
2545        p.setAntiAlias(true);
2546    }
2547
2548    private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) {
2549        int dateNum = mFirstVisibleDate + day;
2550        int x;
2551        if (dateNum > mMonthLength) {
2552            dateNum -= mMonthLength;
2553        }
2554        p.setAntiAlias(true);
2555
2556        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2557        // Draw day of the month
2558        String dateNumStr = String.valueOf(dateNum);
2559        if (mNumDays > 1) {
2560            float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN;
2561
2562            // Draw day of the month
2563            x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN;
2564            p.setTextAlign(Align.RIGHT);
2565            p.setTextSize(DATE_HEADER_FONT_SIZE);
2566
2567            p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT);
2568            canvas.drawText(dateNumStr, x, y, p);
2569
2570            // Draw day of the week
2571            x -= p.measureText(" " + dateNumStr);
2572            p.setTextSize(DAY_HEADER_FONT_SIZE);
2573            p.setTypeface(Typeface.DEFAULT);
2574            canvas.drawText(dayStr, x, y, p);
2575        } else {
2576            float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN;
2577            p.setTextAlign(Align.LEFT);
2578
2579
2580            // Draw day of the week
2581            x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN;
2582            p.setTextSize(DAY_HEADER_FONT_SIZE);
2583            p.setTypeface(Typeface.DEFAULT);
2584            canvas.drawText(dayStr, x, y, p);
2585
2586            // Draw day of the month
2587            x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN;
2588            p.setTextSize(DATE_HEADER_FONT_SIZE);
2589            p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT);
2590            canvas.drawText(dateNumStr, x, y, p);
2591        }
2592    }
2593
2594    private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
2595        Paint.Style savedStyle = p.getStyle();
2596
2597        final float stopX = computeDayLeftPosition(mNumDays);
2598        float y = 0;
2599        final float deltaY = mCellHeight + HOUR_GAP;
2600        int linesIndex = 0;
2601        final float startY = 0;
2602        final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
2603        float x = mHoursWidth;
2604
2605        // Draw the inner horizontal grid lines
2606        p.setColor(mCalendarGridLineInnerHorizontalColor);
2607        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2608        p.setAntiAlias(false);
2609        y = 0;
2610        linesIndex = 0;
2611        for (int hour = 0; hour <= 24; hour++) {
2612            mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2613            mLines[linesIndex++] = y;
2614            mLines[linesIndex++] = stopX;
2615            mLines[linesIndex++] = y;
2616            y += deltaY;
2617        }
2618        if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) {
2619            canvas.drawLines(mLines, 0, linesIndex, p);
2620            linesIndex = 0;
2621            p.setColor(mCalendarGridLineInnerVerticalColor);
2622        }
2623
2624        // Draw the inner vertical grid lines
2625        for (int day = 0; day <= mNumDays; day++) {
2626            x = computeDayLeftPosition(day);
2627            mLines[linesIndex++] = x;
2628            mLines[linesIndex++] = startY;
2629            mLines[linesIndex++] = x;
2630            mLines[linesIndex++] = stopY;
2631        }
2632        canvas.drawLines(mLines, 0, linesIndex, p);
2633
2634        // Restore the saved style.
2635        p.setStyle(savedStyle);
2636        p.setAntiAlias(true);
2637    }
2638
2639    /**
2640     * @param r
2641     * @param canvas
2642     * @param p
2643     */
2644    private void drawBgColors(Rect r, Canvas canvas, Paint p) {
2645        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2646        // Draw the hours background color
2647        r.top = mDestRect.top;
2648        r.bottom = mDestRect.bottom;
2649        r.left = 0;
2650        r.right = mHoursWidth;
2651        p.setColor(mBgColor);
2652        p.setStyle(Style.FILL);
2653        p.setAntiAlias(false);
2654        canvas.drawRect(r, p);
2655
2656        // Draw background for grid area
2657        if (mNumDays == 1 && todayIndex == 0) {
2658            // Draw a white background for the time later than current time
2659            int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2660                    + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2661            if (lineY < mViewStartY + mViewHeight) {
2662                lineY = Math.max(lineY, mViewStartY);
2663                r.left = mHoursWidth;
2664                r.right = mViewWidth;
2665                r.top = lineY;
2666                r.bottom = mViewStartY + mViewHeight;
2667                p.setColor(mFutureBgColor);
2668                canvas.drawRect(r, p);
2669            }
2670        } else if (todayIndex >= 0 && todayIndex < mNumDays) {
2671            // Draw today with a white background for the time later than current time
2672            int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2673                    + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2674            if (lineY < mViewStartY + mViewHeight) {
2675                lineY = Math.max(lineY, mViewStartY);
2676                r.left = computeDayLeftPosition(todayIndex) + 1;
2677                r.right = computeDayLeftPosition(todayIndex + 1);
2678                r.top = lineY;
2679                r.bottom = mViewStartY + mViewHeight;
2680                p.setColor(mFutureBgColor);
2681                canvas.drawRect(r, p);
2682            }
2683
2684            // Paint Tomorrow and later days with future color
2685            if (todayIndex + 1 < mNumDays) {
2686                r.left = computeDayLeftPosition(todayIndex + 1) + 1;
2687                r.right = computeDayLeftPosition(mNumDays);
2688                r.top = mDestRect.top;
2689                r.bottom = mDestRect.bottom;
2690                p.setColor(mFutureBgColor);
2691                canvas.drawRect(r, p);
2692            }
2693        } else if (todayIndex < 0) {
2694            // Future
2695            r.left = computeDayLeftPosition(0) + 1;
2696            r.right = computeDayLeftPosition(mNumDays);
2697            r.top = mDestRect.top;
2698            r.bottom = mDestRect.bottom;
2699            p.setColor(mFutureBgColor);
2700            canvas.drawRect(r, p);
2701        }
2702        p.setAntiAlias(true);
2703    }
2704
2705    Event getSelectedEvent() {
2706        if (mSelectedEvent == null) {
2707            // There is no event at the selected hour, so create a new event.
2708            return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2709                    getSelectedMinutesSinceMidnight());
2710        }
2711        return mSelectedEvent;
2712    }
2713
2714    boolean isEventSelected() {
2715        return (mSelectedEvent != null);
2716    }
2717
2718    Event getNewEvent() {
2719        return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2720                getSelectedMinutesSinceMidnight());
2721    }
2722
2723    static Event getNewEvent(int julianDay, long utcMillis,
2724            int minutesSinceMidnight) {
2725        Event event = Event.newInstance();
2726        event.startDay = julianDay;
2727        event.endDay = julianDay;
2728        event.startMillis = utcMillis;
2729        event.endMillis = event.startMillis + MILLIS_PER_HOUR;
2730        event.startTime = minutesSinceMidnight;
2731        event.endTime = event.startTime + MINUTES_PER_HOUR;
2732        return event;
2733    }
2734
2735    private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
2736        float maxWidthF = 0.0f;
2737
2738        int len = strings.length;
2739        for (int i = 0; i < len; i++) {
2740            float width = p.measureText(strings[i]);
2741            maxWidthF = Math.max(width, maxWidthF);
2742        }
2743        int maxWidth = (int) (maxWidthF + 0.5);
2744        if (maxWidth < currentMax) {
2745            maxWidth = currentMax;
2746        }
2747        return maxWidth;
2748    }
2749
2750    private void saveSelectionPosition(float left, float top, float right, float bottom) {
2751        mPrevBox.left = (int) left;
2752        mPrevBox.right = (int) right;
2753        mPrevBox.top = (int) top;
2754        mPrevBox.bottom = (int) bottom;
2755    }
2756
2757    private Rect getCurrentSelectionPosition() {
2758        Rect box = new Rect();
2759        box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2760        box.bottom = box.top + mCellHeight + HOUR_GAP;
2761        int daynum = mSelectionDay - mFirstJulianDay;
2762        box.left = computeDayLeftPosition(daynum) + 1;
2763        box.right = computeDayLeftPosition(daynum + 1);
2764        return box;
2765    }
2766
2767    private void setupTextRect(Rect r) {
2768        if (r.bottom <= r.top || r.right <= r.left) {
2769            r.bottom = r.top;
2770            r.right = r.left;
2771            return;
2772        }
2773
2774        if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) {
2775            r.top += EVENT_TEXT_TOP_MARGIN;
2776            r.bottom -= EVENT_TEXT_BOTTOM_MARGIN;
2777        }
2778        if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) {
2779            r.left += EVENT_TEXT_LEFT_MARGIN;
2780            r.right -= EVENT_TEXT_RIGHT_MARGIN;
2781        }
2782    }
2783
2784    private void setupAllDayTextRect(Rect r) {
2785        if (r.bottom <= r.top || r.right <= r.left) {
2786            r.bottom = r.top;
2787            r.right = r.left;
2788            return;
2789        }
2790
2791        if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) {
2792            r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN;
2793            r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN;
2794        }
2795        if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) {
2796            r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN;
2797            r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN;
2798        }
2799    }
2800
2801    /**
2802     * Return the layout for a numbered event. Create it if not already existing
2803     */
2804    private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint,
2805            Rect r) {
2806        if (i < 0 || i >= layouts.length) {
2807            return null;
2808        }
2809
2810        StaticLayout layout = layouts[i];
2811        // Check if we have already initialized the StaticLayout and that
2812        // the width hasn't changed (due to vertical resizing which causes
2813        // re-layout of events at min height)
2814        if (layout == null || r.width() != layout.getWidth()) {
2815            SpannableStringBuilder bob = new SpannableStringBuilder();
2816            if (event.title != null) {
2817                // MAX - 1 since we add a space
2818                bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1));
2819                bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0);
2820                bob.append(' ');
2821            }
2822            if (event.location != null) {
2823                bob.append(drawTextSanitizer(event.location.toString(),
2824                        MAX_EVENT_TEXT_LEN - bob.length()));
2825            }
2826
2827            switch (event.selfAttendeeStatus) {
2828                case Attendees.ATTENDEE_STATUS_INVITED:
2829                    paint.setColor(event.color);
2830                    break;
2831                case Attendees.ATTENDEE_STATUS_DECLINED:
2832                    paint.setColor(mEventTextColor);
2833                    paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA);
2834                    break;
2835                case Attendees.ATTENDEE_STATUS_NONE: // Your own events
2836                case Attendees.ATTENDEE_STATUS_ACCEPTED:
2837                case Attendees.ATTENDEE_STATUS_TENTATIVE:
2838                default:
2839                    paint.setColor(mEventTextColor);
2840                    break;
2841            }
2842
2843            // Leave a one pixel boundary on the left and right of the rectangle for the event
2844            layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(),
2845                    Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width());
2846
2847            layouts[i] = layout;
2848        }
2849        layout.getPaint().setAlpha(mEventsAlpha);
2850        return layout;
2851    }
2852
2853    private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) {
2854
2855        p.setTextSize(NORMAL_FONT_SIZE);
2856        p.setTextAlign(Paint.Align.LEFT);
2857        Paint eventTextPaint = mEventTextPaint;
2858
2859        final float startY = DAY_HEADER_HEIGHT;
2860        final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN;
2861        float x = 0;
2862        int linesIndex = 0;
2863
2864        // Draw the inner vertical grid lines
2865        p.setColor(mCalendarGridLineInnerVerticalColor);
2866        x = mHoursWidth;
2867        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2868        // Line bounding the top of the all day area
2869        mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2870        mLines[linesIndex++] = startY;
2871        mLines[linesIndex++] = computeDayLeftPosition(mNumDays);
2872        mLines[linesIndex++] = startY;
2873
2874        for (int day = 0; day <= mNumDays; day++) {
2875            x = computeDayLeftPosition(day);
2876            mLines[linesIndex++] = x;
2877            mLines[linesIndex++] = startY;
2878            mLines[linesIndex++] = x;
2879            mLines[linesIndex++] = stopY;
2880        }
2881        p.setAntiAlias(false);
2882        canvas.drawLines(mLines, 0, linesIndex, p);
2883        p.setStyle(Style.FILL);
2884
2885        int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
2886        int lastDay = firstDay + numDays - 1;
2887        final ArrayList<Event> events = mAllDayEvents;
2888        int numEvents = events.size();
2889        // Whether or not we should draw the more events text
2890        boolean hasMoreEvents = false;
2891        // size of the allDay area
2892        float drawHeight = mAlldayHeight;
2893        // max number of events being drawn in one day of the allday area
2894        float numRectangles = mMaxAlldayEvents;
2895        // Where to cut off drawn allday events
2896        int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN;
2897        // The number of events that weren't drawn in each day
2898        mSkippedAlldayEvents = new int[numDays];
2899        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents &&
2900                mAnimateDayHeight == 0) {
2901            // We draw one fewer event than will fit so that more events text
2902            // can be drawn
2903            numRectangles = mMaxUnexpandedAlldayEventCount - 1;
2904            // We also clip the events above the more events text
2905            allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
2906            hasMoreEvents = true;
2907        } else if (mAnimateDayHeight != 0) {
2908            // clip at the end of the animating space
2909            allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN;
2910        }
2911
2912        int alpha = eventTextPaint.getAlpha();
2913        eventTextPaint.setAlpha(mEventsAlpha);
2914        for (int i = 0; i < numEvents; i++) {
2915            Event event = events.get(i);
2916            int startDay = event.startDay;
2917            int endDay = event.endDay;
2918            if (startDay > lastDay || endDay < firstDay) {
2919                continue;
2920            }
2921            if (startDay < firstDay) {
2922                startDay = firstDay;
2923            }
2924            if (endDay > lastDay) {
2925                endDay = lastDay;
2926            }
2927            int startIndex = startDay - firstDay;
2928            int endIndex = endDay - firstDay;
2929            float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight :
2930                    drawHeight / numRectangles;
2931
2932            // Prevent a single event from getting too big
2933            if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
2934                height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
2935            }
2936
2937            // Leave a one-pixel space between the vertical day lines and the
2938            // event rectangle.
2939            event.left = computeDayLeftPosition(startIndex);
2940            event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP;
2941            event.top = y + height * event.getColumn();
2942            event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN;
2943            if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2944                // check if we should skip this event. We skip if it starts
2945                // after the clip bound or ends after the skip bound and we're
2946                // not animating.
2947                if (event.top >= allDayEventClip) {
2948                    incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2949                    continue;
2950                } else if (event.bottom > allDayEventClip) {
2951                    if (hasMoreEvents) {
2952                        incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2953                        continue;
2954                    }
2955                    event.bottom = allDayEventClip;
2956                }
2957            }
2958            Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top,
2959                    (int) event.bottom);
2960            setupAllDayTextRect(r);
2961            StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r);
2962            drawEventText(layout, r, canvas, r.top, r.bottom, true);
2963
2964            // Check if this all-day event intersects the selected day
2965            if (mSelectionAllday && mComputeSelectedEvents) {
2966                if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
2967                    mSelectedEvents.add(event);
2968                }
2969            }
2970        }
2971        eventTextPaint.setAlpha(alpha);
2972
2973        if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) {
2974            // If the more allday text should be visible, draw it.
2975            alpha = p.getAlpha();
2976            p.setAlpha(mEventsAlpha);
2977            p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor);
2978            for (int i = 0; i < mSkippedAlldayEvents.length; i++) {
2979                if (mSkippedAlldayEvents[i] > 0) {
2980                    drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p);
2981                }
2982            }
2983            p.setAlpha(alpha);
2984        }
2985
2986        if (mSelectionAllday) {
2987            // Compute the neighbors for the list of all-day events that
2988            // intersect the selected day.
2989            computeAllDayNeighbors();
2990
2991            // Set the selection position to zero so that when we move down
2992            // to the normal event area, we will highlight the topmost event.
2993            saveSelectionPosition(0f, 0f, 0f, 0f);
2994        }
2995    }
2996
2997    // Helper method for counting the number of allday events skipped on each day
2998    private void incrementSkipCount(int[] counts, int startIndex, int endIndex) {
2999        if (counts == null || startIndex < 0 || endIndex > counts.length) {
3000            return;
3001        }
3002        for (int i = startIndex; i <= endIndex; i++) {
3003            counts[i]++;
3004        }
3005    }
3006
3007    // Draws the "box +n" text for hidden allday events
3008    protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) {
3009        int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN;
3010        int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f
3011                * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN);
3012        Rect r = mRect;
3013        r.top = y;
3014        r.left = x;
3015        r.bottom = y + EVENT_SQUARE_WIDTH;
3016        r.right = x + EVENT_SQUARE_WIDTH;
3017        p.setColor(mMoreEventsTextColor);
3018        p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH);
3019        p.setStyle(Style.STROKE);
3020        p.setAntiAlias(false);
3021        canvas.drawRect(r, p);
3022        p.setAntiAlias(true);
3023        p.setStyle(Style.FILL);
3024        p.setTextSize(EVENT_TEXT_FONT_SIZE);
3025        String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents);
3026        y += EVENT_SQUARE_WIDTH;
3027        x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING;
3028        canvas.drawText(String.format(text, remainingEvents), x, y, p);
3029    }
3030
3031    private void computeAllDayNeighbors() {
3032        int len = mSelectedEvents.size();
3033        if (len == 0 || mSelectedEvent != null) {
3034            return;
3035        }
3036
3037        // First, clear all the links
3038        for (int ii = 0; ii < len; ii++) {
3039            Event ev = mSelectedEvents.get(ii);
3040            ev.nextUp = null;
3041            ev.nextDown = null;
3042            ev.nextLeft = null;
3043            ev.nextRight = null;
3044        }
3045
3046        // For each event in the selected event list "mSelectedEvents", find
3047        // its neighbors in the up and down directions. This could be done
3048        // more efficiently by sorting on the Event.getColumn() field, but
3049        // the list is expected to be very small.
3050
3051        // Find the event in the same row as the previously selected all-day
3052        // event, if any.
3053        int startPosition = -1;
3054        if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) {
3055            startPosition = mPrevSelectedEvent.getColumn();
3056        }
3057        int maxPosition = -1;
3058        Event startEvent = null;
3059        Event maxPositionEvent = null;
3060        for (int ii = 0; ii < len; ii++) {
3061            Event ev = mSelectedEvents.get(ii);
3062            int position = ev.getColumn();
3063            if (position == startPosition) {
3064                startEvent = ev;
3065            } else if (position > maxPosition) {
3066                maxPositionEvent = ev;
3067                maxPosition = position;
3068            }
3069            for (int jj = 0; jj < len; jj++) {
3070                if (jj == ii) {
3071                    continue;
3072                }
3073                Event neighbor = mSelectedEvents.get(jj);
3074                int neighborPosition = neighbor.getColumn();
3075                if (neighborPosition == position - 1) {
3076                    ev.nextUp = neighbor;
3077                } else if (neighborPosition == position + 1) {
3078                    ev.nextDown = neighbor;
3079                }
3080            }
3081        }
3082        if (startEvent != null) {
3083            setSelectedEvent(startEvent);
3084        } else {
3085            setSelectedEvent(maxPositionEvent);
3086        }
3087    }
3088
3089    private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) {
3090        Paint eventTextPaint = mEventTextPaint;
3091        int left = computeDayLeftPosition(dayIndex) + 1;
3092        int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1;
3093        int cellHeight = mCellHeight;
3094
3095        // Use the selected hour as the selection region
3096        Rect selectionArea = mSelectionRect;
3097        selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
3098        selectionArea.bottom = selectionArea.top + cellHeight;
3099        selectionArea.left = left;
3100        selectionArea.right = selectionArea.left + cellWidth;
3101
3102        final ArrayList<Event> events = mEvents;
3103        int numEvents = events.size();
3104        EventGeometry geometry = mEventGeometry;
3105
3106        final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight;
3107
3108        int alpha = eventTextPaint.getAlpha();
3109        eventTextPaint.setAlpha(mEventsAlpha);
3110        for (int i = 0; i < numEvents; i++) {
3111            Event event = events.get(i);
3112            if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
3113                continue;
3114            }
3115
3116            // Don't draw it if it is not visible
3117            if (event.bottom < mViewStartY || event.top > viewEndY) {
3118                continue;
3119            }
3120
3121            if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents
3122                    && geometry.eventIntersectsSelection(event, selectionArea)) {
3123                mSelectedEvents.add(event);
3124            }
3125
3126            Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY);
3127            setupTextRect(r);
3128
3129            // Don't draw text if it is not visible
3130            if (r.top > viewEndY || r.bottom < mViewStartY) {
3131                continue;
3132            }
3133            StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r);
3134            // TODO: not sure why we are 4 pixels off
3135            drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight
3136                    - DAY_HEADER_HEIGHT - mAlldayHeight, false);
3137        }
3138        eventTextPaint.setAlpha(alpha);
3139
3140        if (date == mSelectionDay && !mSelectionAllday && isFocused()
3141                && mSelectionMode != SELECTION_HIDDEN) {
3142            computeNeighbors();
3143        }
3144    }
3145
3146    // Computes the "nearest" neighbor event in four directions (left, right,
3147    // up, down) for each of the events in the mSelectedEvents array.
3148    private void computeNeighbors() {
3149        int len = mSelectedEvents.size();
3150        if (len == 0 || mSelectedEvent != null) {
3151            return;
3152        }
3153
3154        // First, clear all the links
3155        for (int ii = 0; ii < len; ii++) {
3156            Event ev = mSelectedEvents.get(ii);
3157            ev.nextUp = null;
3158            ev.nextDown = null;
3159            ev.nextLeft = null;
3160            ev.nextRight = null;
3161        }
3162
3163        Event startEvent = mSelectedEvents.get(0);
3164        int startEventDistance1 = 100000; // any large number
3165        int startEventDistance2 = 100000; // any large number
3166        int prevLocation = FROM_NONE;
3167        int prevTop;
3168        int prevBottom;
3169        int prevLeft;
3170        int prevRight;
3171        int prevCenter = 0;
3172        Rect box = getCurrentSelectionPosition();
3173        if (mPrevSelectedEvent != null) {
3174            prevTop = (int) mPrevSelectedEvent.top;
3175            prevBottom = (int) mPrevSelectedEvent.bottom;
3176            prevLeft = (int) mPrevSelectedEvent.left;
3177            prevRight = (int) mPrevSelectedEvent.right;
3178            // Check if the previously selected event intersects the previous
3179            // selection box. (The previously selected event may be from a
3180            // much older selection box.)
3181            if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
3182                    || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
3183                mPrevSelectedEvent = null;
3184                prevTop = mPrevBox.top;
3185                prevBottom = mPrevBox.bottom;
3186                prevLeft = mPrevBox.left;
3187                prevRight = mPrevBox.right;
3188            } else {
3189                // Clip the top and bottom to the previous selection box.
3190                if (prevTop < mPrevBox.top) {
3191                    prevTop = mPrevBox.top;
3192                }
3193                if (prevBottom > mPrevBox.bottom) {
3194                    prevBottom = mPrevBox.bottom;
3195                }
3196            }
3197        } else {
3198            // Just use the previously drawn selection box
3199            prevTop = mPrevBox.top;
3200            prevBottom = mPrevBox.bottom;
3201            prevLeft = mPrevBox.left;
3202            prevRight = mPrevBox.right;
3203        }
3204
3205        // Figure out where we came from and compute the center of that area.
3206        if (prevLeft >= box.right) {
3207            // The previously selected event was to the right of us.
3208            prevLocation = FROM_RIGHT;
3209            prevCenter = (prevTop + prevBottom) / 2;
3210        } else if (prevRight <= box.left) {
3211            // The previously selected event was to the left of us.
3212            prevLocation = FROM_LEFT;
3213            prevCenter = (prevTop + prevBottom) / 2;
3214        } else if (prevBottom <= box.top) {
3215            // The previously selected event was above us.
3216            prevLocation = FROM_ABOVE;
3217            prevCenter = (prevLeft + prevRight) / 2;
3218        } else if (prevTop >= box.bottom) {
3219            // The previously selected event was below us.
3220            prevLocation = FROM_BELOW;
3221            prevCenter = (prevLeft + prevRight) / 2;
3222        }
3223
3224        // For each event in the selected event list "mSelectedEvents", search
3225        // all the other events in that list for the nearest neighbor in 4
3226        // directions.
3227        for (int ii = 0; ii < len; ii++) {
3228            Event ev = mSelectedEvents.get(ii);
3229
3230            int startTime = ev.startTime;
3231            int endTime = ev.endTime;
3232            int left = (int) ev.left;
3233            int right = (int) ev.right;
3234            int top = (int) ev.top;
3235            if (top < box.top) {
3236                top = box.top;
3237            }
3238            int bottom = (int) ev.bottom;
3239            if (bottom > box.bottom) {
3240                bottom = box.bottom;
3241            }
3242//            if (false) {
3243//                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
3244//                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
3245//                if (DateFormat.is24HourFormat(mContext)) {
3246//                    flags |= DateUtils.FORMAT_24HOUR;
3247//                }
3248//                String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis,
3249//                        ev.endMillis, flags);
3250//                Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: "
3251//                        + bottom + " ev: " + timeRange + " " + ev.title);
3252//            }
3253            int upDistanceMin = 10000; // any large number
3254            int downDistanceMin = 10000; // any large number
3255            int leftDistanceMin = 10000; // any large number
3256            int rightDistanceMin = 10000; // any large number
3257            Event upEvent = null;
3258            Event downEvent = null;
3259            Event leftEvent = null;
3260            Event rightEvent = null;
3261
3262            // Pick the starting event closest to the previously selected event,
3263            // if any. distance1 takes precedence over distance2.
3264            int distance1 = 0;
3265            int distance2 = 0;
3266            if (prevLocation == FROM_ABOVE) {
3267                if (left >= prevCenter) {
3268                    distance1 = left - prevCenter;
3269                } else if (right <= prevCenter) {
3270                    distance1 = prevCenter - right;
3271                }
3272                distance2 = top - prevBottom;
3273            } else if (prevLocation == FROM_BELOW) {
3274                if (left >= prevCenter) {
3275                    distance1 = left - prevCenter;
3276                } else if (right <= prevCenter) {
3277                    distance1 = prevCenter - right;
3278                }
3279                distance2 = prevTop - bottom;
3280            } else if (prevLocation == FROM_LEFT) {
3281                if (bottom <= prevCenter) {
3282                    distance1 = prevCenter - bottom;
3283                } else if (top >= prevCenter) {
3284                    distance1 = top - prevCenter;
3285                }
3286                distance2 = left - prevRight;
3287            } else if (prevLocation == FROM_RIGHT) {
3288                if (bottom <= prevCenter) {
3289                    distance1 = prevCenter - bottom;
3290                } else if (top >= prevCenter) {
3291                    distance1 = top - prevCenter;
3292                }
3293                distance2 = prevLeft - right;
3294            }
3295            if (distance1 < startEventDistance1
3296                    || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
3297                startEvent = ev;
3298                startEventDistance1 = distance1;
3299                startEventDistance2 = distance2;
3300            }
3301
3302            // For each neighbor, figure out if it is above or below or left
3303            // or right of me and compute the distance.
3304            for (int jj = 0; jj < len; jj++) {
3305                if (jj == ii) {
3306                    continue;
3307                }
3308                Event neighbor = mSelectedEvents.get(jj);
3309                int neighborLeft = (int) neighbor.left;
3310                int neighborRight = (int) neighbor.right;
3311                if (neighbor.endTime <= startTime) {
3312                    // This neighbor is entirely above me.
3313                    // If we overlap the same column, then compute the distance.
3314                    if (neighborLeft < right && neighborRight > left) {
3315                        int distance = startTime - neighbor.endTime;
3316                        if (distance < upDistanceMin) {
3317                            upDistanceMin = distance;
3318                            upEvent = neighbor;
3319                        } else if (distance == upDistanceMin) {
3320                            int center = (left + right) / 2;
3321                            int currentDistance = 0;
3322                            int currentLeft = (int) upEvent.left;
3323                            int currentRight = (int) upEvent.right;
3324                            if (currentRight <= center) {
3325                                currentDistance = center - currentRight;
3326                            } else if (currentLeft >= center) {
3327                                currentDistance = currentLeft - center;
3328                            }
3329
3330                            int neighborDistance = 0;
3331                            if (neighborRight <= center) {
3332                                neighborDistance = center - neighborRight;
3333                            } else if (neighborLeft >= center) {
3334                                neighborDistance = neighborLeft - center;
3335                            }
3336                            if (neighborDistance < currentDistance) {
3337                                upDistanceMin = distance;
3338                                upEvent = neighbor;
3339                            }
3340                        }
3341                    }
3342                } else if (neighbor.startTime >= endTime) {
3343                    // This neighbor is entirely below me.
3344                    // If we overlap the same column, then compute the distance.
3345                    if (neighborLeft < right && neighborRight > left) {
3346                        int distance = neighbor.startTime - endTime;
3347                        if (distance < downDistanceMin) {
3348                            downDistanceMin = distance;
3349                            downEvent = neighbor;
3350                        } else if (distance == downDistanceMin) {
3351                            int center = (left + right) / 2;
3352                            int currentDistance = 0;
3353                            int currentLeft = (int) downEvent.left;
3354                            int currentRight = (int) downEvent.right;
3355                            if (currentRight <= center) {
3356                                currentDistance = center - currentRight;
3357                            } else if (currentLeft >= center) {
3358                                currentDistance = currentLeft - center;
3359                            }
3360
3361                            int neighborDistance = 0;
3362                            if (neighborRight <= center) {
3363                                neighborDistance = center - neighborRight;
3364                            } else if (neighborLeft >= center) {
3365                                neighborDistance = neighborLeft - center;
3366                            }
3367                            if (neighborDistance < currentDistance) {
3368                                downDistanceMin = distance;
3369                                downEvent = neighbor;
3370                            }
3371                        }
3372                    }
3373                }
3374
3375                if (neighborLeft >= right) {
3376                    // This neighbor is entirely to the right of me.
3377                    // Take the closest neighbor in the y direction.
3378                    int center = (top + bottom) / 2;
3379                    int distance = 0;
3380                    int neighborBottom = (int) neighbor.bottom;
3381                    int neighborTop = (int) neighbor.top;
3382                    if (neighborBottom <= center) {
3383                        distance = center - neighborBottom;
3384                    } else if (neighborTop >= center) {
3385                        distance = neighborTop - center;
3386                    }
3387                    if (distance < rightDistanceMin) {
3388                        rightDistanceMin = distance;
3389                        rightEvent = neighbor;
3390                    } else if (distance == rightDistanceMin) {
3391                        // Pick the closest in the x direction
3392                        int neighborDistance = neighborLeft - right;
3393                        int currentDistance = (int) rightEvent.left - right;
3394                        if (neighborDistance < currentDistance) {
3395                            rightDistanceMin = distance;
3396                            rightEvent = neighbor;
3397                        }
3398                    }
3399                } else if (neighborRight <= left) {
3400                    // This neighbor is entirely to the left of me.
3401                    // Take the closest neighbor in the y direction.
3402                    int center = (top + bottom) / 2;
3403                    int distance = 0;
3404                    int neighborBottom = (int) neighbor.bottom;
3405                    int neighborTop = (int) neighbor.top;
3406                    if (neighborBottom <= center) {
3407                        distance = center - neighborBottom;
3408                    } else if (neighborTop >= center) {
3409                        distance = neighborTop - center;
3410                    }
3411                    if (distance < leftDistanceMin) {
3412                        leftDistanceMin = distance;
3413                        leftEvent = neighbor;
3414                    } else if (distance == leftDistanceMin) {
3415                        // Pick the closest in the x direction
3416                        int neighborDistance = left - neighborRight;
3417                        int currentDistance = left - (int) leftEvent.right;
3418                        if (neighborDistance < currentDistance) {
3419                            leftDistanceMin = distance;
3420                            leftEvent = neighbor;
3421                        }
3422                    }
3423                }
3424            }
3425            ev.nextUp = upEvent;
3426            ev.nextDown = downEvent;
3427            ev.nextLeft = leftEvent;
3428            ev.nextRight = rightEvent;
3429        }
3430        setSelectedEvent(startEvent);
3431    }
3432
3433    private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint,
3434            int visibleTop, int visibleBot) {
3435        // Draw the Event Rect
3436        Rect r = mRect;
3437        r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop);
3438        r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot);
3439        r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN;
3440        r.right = (int) event.right;
3441
3442        int color;
3443        if (event == mClickedEvent) {
3444                color = mClickedColor;
3445        } else {
3446            color = event.color;
3447        }
3448
3449        switch (event.selfAttendeeStatus) {
3450            case Attendees.ATTENDEE_STATUS_INVITED:
3451                if (event != mClickedEvent) {
3452                    p.setStyle(Style.STROKE);
3453                }
3454                break;
3455            case Attendees.ATTENDEE_STATUS_DECLINED:
3456                if (event != mClickedEvent) {
3457                    color = Utils.getDeclinedColorFromColor(color);
3458                }
3459            case Attendees.ATTENDEE_STATUS_NONE: // Your own events
3460            case Attendees.ATTENDEE_STATUS_ACCEPTED:
3461            case Attendees.ATTENDEE_STATUS_TENTATIVE:
3462            default:
3463                p.setStyle(Style.FILL_AND_STROKE);
3464                break;
3465        }
3466
3467        p.setAntiAlias(false);
3468
3469        int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f);
3470        int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f);
3471        r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop);
3472        r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke,
3473                visibleBot);
3474        r.left += floorHalfStroke;
3475        r.right -= ceilHalfStroke;
3476        p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH);
3477        p.setColor(color);
3478        int alpha = p.getAlpha();
3479        p.setAlpha(mEventsAlpha);
3480        canvas.drawRect(r, p);
3481        p.setAlpha(alpha);
3482        p.setStyle(Style.FILL);
3483
3484        // If this event is selected, then use the selection color
3485        if (mSelectedEvent == event && mClickedEvent != null) {
3486            boolean paintIt = false;
3487            color = 0;
3488            if (mSelectionMode == SELECTION_PRESSED) {
3489                // Also, remember the last selected event that we drew
3490                mPrevSelectedEvent = event;
3491                color = mPressedColor;
3492                paintIt = true;
3493            } else if (mSelectionMode == SELECTION_SELECTED) {
3494                // Also, remember the last selected event that we drew
3495                mPrevSelectedEvent = event;
3496                color = mPressedColor;
3497                paintIt = true;
3498            }
3499
3500            if (paintIt) {
3501                p.setColor(color);
3502                canvas.drawRect(r, p);
3503            }
3504            p.setAntiAlias(true);
3505        }
3506
3507        // Draw cal color square border
3508        // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET;
3509        // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET;
3510        // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1;
3511        // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1;
3512        // p.setColor(0xFFFFFFFF);
3513        // canvas.drawRect(r, p);
3514
3515        // Draw cal color
3516        // r.top++;
3517        // r.left++;
3518        // r.bottom--;
3519        // r.right--;
3520        // p.setColor(event.color);
3521        // canvas.drawRect(r, p);
3522
3523        // Setup rect for drawEventText which follows
3524        r.top = (int) event.top + EVENT_RECT_TOP_MARGIN;
3525        r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN;
3526        r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN;
3527        r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN;
3528        return r;
3529    }
3530
3531    private final Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
3532
3533    // Sanitize a string before passing it to drawText or else we get little
3534    // squares. For newlines and tabs before a comma, delete the character.
3535    // Otherwise, just replace them with a space.
3536    private String drawTextSanitizer(String string, int maxEventTextLen) {
3537        Matcher m = drawTextSanitizerFilter.matcher(string);
3538        string = m.replaceAll(",");
3539
3540        int len = string.length();
3541        if (maxEventTextLen <= 0) {
3542            string = "";
3543            len = 0;
3544        } else if (len > maxEventTextLen) {
3545            string = string.substring(0, maxEventTextLen);
3546            len = maxEventTextLen;
3547        }
3548
3549        return string.replace('\n', ' ');
3550    }
3551
3552    private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top,
3553            int bottom, boolean center) {
3554        // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging
3555
3556        int width = rect.right - rect.left;
3557        int height = rect.bottom - rect.top;
3558
3559        // If the rectangle is too small for text, then return
3560        if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) {
3561            return;
3562        }
3563
3564        int totalLineHeight = 0;
3565        int lineCount = eventLayout.getLineCount();
3566        for (int i = 0; i < lineCount; i++) {
3567            int lineBottom = eventLayout.getLineBottom(i);
3568            if (lineBottom <= height) {
3569                totalLineHeight = lineBottom;
3570            } else {
3571                break;
3572            }
3573        }
3574
3575        if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) {
3576            return;
3577        }
3578
3579        // Use a StaticLayout to format the string.
3580        canvas.save();
3581      //  canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2));
3582        int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0;
3583        canvas.translate(rect.left, rect.top + padding);
3584        rect.left = 0;
3585        rect.right = width;
3586        rect.top = 0;
3587        rect.bottom = totalLineHeight;
3588
3589        // There's a bug somewhere. If this rect is outside of a previous
3590        // cliprect, this becomes a no-op. What happens is that the text draw
3591        // past the event rect. The current fix is to not draw the staticLayout
3592        // at all if it is completely out of bound.
3593        canvas.clipRect(rect);
3594        eventLayout.draw(canvas);
3595        canvas.restore();
3596    }
3597
3598    // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it
3599    // doesn't work well with hardware acceleration
3600//    private void drawEmptyRect(Canvas canvas, Rect r, int color) {
3601//        int linesIndex = 0;
3602//        mLines[linesIndex++] = r.left;
3603//        mLines[linesIndex++] = r.top;
3604//        mLines[linesIndex++] = r.right;
3605//        mLines[linesIndex++] = r.top;
3606//
3607//        mLines[linesIndex++] = r.left;
3608//        mLines[linesIndex++] = r.bottom;
3609//        mLines[linesIndex++] = r.right;
3610//        mLines[linesIndex++] = r.bottom;
3611//
3612//        mLines[linesIndex++] = r.left;
3613//        mLines[linesIndex++] = r.top;
3614//        mLines[linesIndex++] = r.left;
3615//        mLines[linesIndex++] = r.bottom;
3616//
3617//        mLines[linesIndex++] = r.right;
3618//        mLines[linesIndex++] = r.top;
3619//        mLines[linesIndex++] = r.right;
3620//        mLines[linesIndex++] = r.bottom;
3621//        mPaint.setColor(color);
3622//        canvas.drawLines(mLines, 0, linesIndex, mPaint);
3623//    }
3624
3625    private void updateEventDetails() {
3626        if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
3627                || mSelectionMode == SELECTION_LONGPRESS) {
3628            mPopup.dismiss();
3629            return;
3630        }
3631        if (mLastPopupEventID == mSelectedEvent.id) {
3632            return;
3633        }
3634
3635        mLastPopupEventID = mSelectedEvent.id;
3636
3637        // Remove any outstanding callbacks to dismiss the popup.
3638        mHandler.removeCallbacks(mDismissPopup);
3639
3640        Event event = mSelectedEvent;
3641        TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
3642        titleView.setText(event.title);
3643
3644        ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
3645        imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
3646
3647        imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
3648        imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
3649
3650        int flags;
3651        if (event.allDay) {
3652            flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
3653                    | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
3654        } else {
3655            flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
3656                    | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
3657                    | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
3658        }
3659        if (DateFormat.is24HourFormat(mContext)) {
3660            flags |= DateUtils.FORMAT_24HOUR;
3661        }
3662        String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis,
3663                flags);
3664        TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
3665        timeView.setText(timeRange);
3666
3667        TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
3668        final boolean empty = TextUtils.isEmpty(event.location);
3669        whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
3670        if (!empty) whereView.setText(event.location);
3671
3672        mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
3673        mHandler.postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
3674    }
3675
3676    // The following routines are called from the parent activity when certain
3677    // touch events occur.
3678    private void doDown(MotionEvent ev) {
3679        mTouchMode = TOUCH_MODE_DOWN;
3680        mViewStartX = 0;
3681        mOnFlingCalled = false;
3682        mHandler.removeCallbacks(mContinueScroll);
3683        int x = (int) ev.getX();
3684        int y = (int) ev.getY();
3685
3686        // Save selection information: we use setSelectionFromPosition to find the selected event
3687        // in order to show the "clicked" color. But since it is also setting the selected info
3688        // for new events, we need to restore the old info after calling the function.
3689        Event oldSelectedEvent = mSelectedEvent;
3690        int oldSelectionDay = mSelectionDay;
3691        int oldSelectionHour = mSelectionHour;
3692        if (setSelectionFromPosition(x, y, false)) {
3693            // If a time was selected (a blue selection box is visible) and the click location
3694            // is in the selected time, do not show a click on an event to prevent a situation
3695            // of both a selection and an event are clicked when they overlap.
3696            boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN)
3697                    && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour;
3698            if (!pressedSelected && mSelectedEvent != null) {
3699                mSavedClickedEvent = mSelectedEvent;
3700                mDownTouchTime = System.currentTimeMillis();
3701                postDelayed (mSetClick,mOnDownDelay);
3702            } else {
3703                eventClickCleanup();
3704            }
3705        }
3706        mSelectedEvent = oldSelectedEvent;
3707        mSelectionDay = oldSelectionDay;
3708        mSelectionHour = oldSelectionHour;
3709        invalidate();
3710    }
3711
3712    // Kicks off all the animations when the expand allday area is tapped
3713    private void doExpandAllDayClick() {
3714        mShowAllAllDayEvents = !mShowAllAllDayEvents;
3715
3716        ObjectAnimator.setFrameDelay(0);
3717
3718        // Determine the starting height
3719        if (mAnimateDayHeight == 0) {
3720            mAnimateDayHeight = mShowAllAllDayEvents ?
3721                    mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight;
3722        }
3723        // Cancel current animations
3724        mCancellingAnimations = true;
3725        if (mAlldayAnimator != null) {
3726            mAlldayAnimator.cancel();
3727        }
3728        if (mAlldayEventAnimator != null) {
3729            mAlldayEventAnimator.cancel();
3730        }
3731        if (mMoreAlldayEventsAnimator != null) {
3732            mMoreAlldayEventsAnimator.cancel();
3733        }
3734        mCancellingAnimations = false;
3735        // get new animators
3736        mAlldayAnimator = getAllDayAnimator();
3737        mAlldayEventAnimator = getAllDayEventAnimator();
3738        mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this,
3739                    "moreAllDayEventsTextAlpha",
3740                    mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0,
3741                    mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA);
3742
3743        // Set up delays and start the animators
3744        mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0);
3745        mAlldayAnimator.start();
3746        mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION);
3747        mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION);
3748        mMoreAlldayEventsAnimator.start();
3749        if (mAlldayEventAnimator != null) {
3750            // This is the only animator that can return null, so check it
3751            mAlldayEventAnimator
3752                    .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0);
3753            mAlldayEventAnimator.start();
3754        }
3755    }
3756
3757    /**
3758     * Figures out the initial heights for allDay events and space when
3759     * a view is being set up.
3760     */
3761    public void initAllDayHeights() {
3762        if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) {
3763            return;
3764        }
3765        if (mShowAllAllDayEvents) {
3766            int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3767            maxADHeight = Math.min(maxADHeight,
3768                    (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3769            mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents;
3770        } else {
3771            mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
3772        }
3773    }
3774
3775    // Sets up an animator for changing the height of allday events
3776    private ObjectAnimator getAllDayEventAnimator() {
3777        // First calculate the absolute max height
3778        int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3779        // Now expand to fit but not beyond the absolute max
3780        maxADHeight =
3781                Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3782        // calculate the height of individual events in order to fit
3783        int fitHeight = maxADHeight / mMaxAlldayEvents;
3784        int currentHeight = mAnimateDayEventHeight;
3785        int desiredHeight =
3786                mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
3787        // if there's nothing to animate just return
3788        if (currentHeight == desiredHeight) {
3789            return null;
3790        }
3791
3792        // Set up the animator with the calculated values
3793        ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight",
3794                currentHeight, desiredHeight);
3795        animator.setDuration(ANIMATION_DURATION);
3796        return animator;
3797    }
3798
3799    // Sets up an animator for changing the height of the allday area
3800    private ObjectAnimator getAllDayAnimator() {
3801        // Calculate the absolute max height
3802        int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
3803        // Find the desired height but don't exceed abs max
3804        maxADHeight =
3805                Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT));
3806        // calculate the current and desired heights
3807        int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight;
3808        int desiredHeight = mShowAllAllDayEvents ? maxADHeight :
3809                (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1);
3810
3811        // Set up the animator with the calculated values
3812        ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight",
3813                currentHeight, desiredHeight);
3814        animator.setDuration(ANIMATION_DURATION);
3815
3816        animator.addListener(new AnimatorListenerAdapter() {
3817            @Override
3818            public void onAnimationEnd(Animator animation) {
3819                if (!mCancellingAnimations) {
3820                    // when finished, set this to 0 to signify not animating
3821                    mAnimateDayHeight = 0;
3822                    mUseExpandIcon = !mShowAllAllDayEvents;
3823                }
3824                mRemeasure = true;
3825                invalidate();
3826            }
3827        });
3828        return animator;
3829    }
3830
3831    // setter for the 'box +n' alpha text used by the animator
3832    public void setMoreAllDayEventsTextAlpha(int alpha) {
3833        mMoreAlldayEventsTextAlpha = alpha;
3834        invalidate();
3835    }
3836
3837    // setter for the height of the allday area used by the animator
3838    public void setAnimateDayHeight(int height) {
3839        mAnimateDayHeight = height;
3840        mRemeasure = true;
3841        invalidate();
3842    }
3843
3844    // setter for the height of allday events used by the animator
3845    public void setAnimateDayEventHeight(int height) {
3846        mAnimateDayEventHeight = height;
3847        mRemeasure = true;
3848        invalidate();
3849    }
3850
3851    private void doSingleTapUp(MotionEvent ev) {
3852        if (!mHandleActionUp || mScrolling) {
3853            return;
3854        }
3855
3856        int x = (int) ev.getX();
3857        int y = (int) ev.getY();
3858        int selectedDay = mSelectionDay;
3859        int selectedHour = mSelectionHour;
3860
3861        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
3862            // check if the tap was in the allday expansion area
3863            int bottom = mFirstCell;
3864            if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight)
3865                    || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom &&
3866                            y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) {
3867                doExpandAllDayClick();
3868                return;
3869            }
3870        }
3871
3872        boolean validPosition = setSelectionFromPosition(x, y, false);
3873        if (!validPosition) {
3874            if (y < DAY_HEADER_HEIGHT) {
3875                Time selectedTime = new Time(mBaseDate);
3876                selectedTime.setJulianDay(mSelectionDay);
3877                selectedTime.hour = mSelectionHour;
3878                selectedTime.normalize(true /* ignore isDst */);
3879                mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1,
3880                        ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null);
3881            }
3882            return;
3883        }
3884
3885        boolean hasSelection = mSelectionMode != SELECTION_HIDDEN;
3886        boolean pressedSelected = (hasSelection || mTouchExplorationEnabled)
3887                && selectedDay == mSelectionDay && selectedHour == mSelectionHour;
3888
3889        if (pressedSelected && mSavedClickedEvent == null) {
3890            // If the tap is on an already selected hour slot, then create a new
3891            // event
3892            long extraLong = 0;
3893            if (mSelectionAllday) {
3894                extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
3895            }
3896            mSelectionMode = SELECTION_SELECTED;
3897            mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1,
3898                    getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(),
3899                    extraLong, -1);
3900        } else if (mSelectedEvent != null) {
3901            // If the tap is on an event, launch the "View event" view
3902            if (mIsAccessibilityEnabled) {
3903                mAccessibilityMgr.interrupt();
3904            }
3905
3906            mSelectionMode = SELECTION_HIDDEN;
3907
3908            int yLocation =
3909                (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2);
3910            // Y location is affected by the position of the event in the scrolling
3911            // view (mViewStartY) and the presence of all day events (mFirstCell)
3912            if (!mSelectedEvent.allDay) {
3913                yLocation += (mFirstCell - mViewStartY);
3914            }
3915            mClickedYLocation = yLocation;
3916            long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) -
3917                    (System.currentTimeMillis() - mDownTouchTime);
3918            if (clearDelay > 0) {
3919                this.postDelayed(mClearClick, clearDelay);
3920            } else {
3921                this.post(mClearClick);
3922            }
3923        } else {
3924            // Select time
3925            Time startTime = new Time(mBaseDate);
3926            startTime.setJulianDay(mSelectionDay);
3927            startTime.hour = mSelectionHour;
3928            startTime.normalize(true /* ignore isDst */);
3929
3930            Time endTime = new Time(startTime);
3931            endTime.hour++;
3932
3933            mSelectionMode = SELECTION_SELECTED;
3934            mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT,
3935                    CalendarController.EXTRA_GOTO_TIME, null, null);
3936        }
3937        invalidate();
3938    }
3939
3940    private void doLongPress(MotionEvent ev) {
3941        eventClickCleanup();
3942        if (mScrolling) {
3943            return;
3944        }
3945
3946        // Scale gesture in progress
3947        if (mStartingSpanY != 0) {
3948            return;
3949        }
3950
3951        int x = (int) ev.getX();
3952        int y = (int) ev.getY();
3953
3954        boolean validPosition = setSelectionFromPosition(x, y, false);
3955        if (!validPosition) {
3956            // return if the touch wasn't on an area of concern
3957            return;
3958        }
3959
3960        mSelectionMode = SELECTION_LONGPRESS;
3961        invalidate();
3962        performLongClick();
3963    }
3964
3965    private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
3966        cancelAnimation();
3967        if (mStartingScroll) {
3968            mInitialScrollX = 0;
3969            mInitialScrollY = 0;
3970            mStartingScroll = false;
3971        }
3972
3973        mInitialScrollX += deltaX;
3974        mInitialScrollY += deltaY;
3975        int distanceX = (int) mInitialScrollX;
3976        int distanceY = (int) mInitialScrollY;
3977
3978        final float focusY = getAverageY(e2);
3979        if (mRecalCenterHour) {
3980            // Calculate the hour that correspond to the average of the Y touch points
3981            mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight)
3982                    / (mCellHeight + DAY_GAP);
3983            mRecalCenterHour = false;
3984        }
3985
3986        // If we haven't figured out the predominant scroll direction yet,
3987        // then do it now.
3988        if (mTouchMode == TOUCH_MODE_DOWN) {
3989            int absDistanceX = Math.abs(distanceX);
3990            int absDistanceY = Math.abs(distanceY);
3991            mScrollStartY = mViewStartY;
3992            mPreviousDirection = 0;
3993
3994            if (absDistanceX > absDistanceY) {
3995                int slopFactor = mScaleGestureDetector.isInProgress() ? 20 : 2;
3996                if (absDistanceX > mScaledPagingTouchSlop * slopFactor) {
3997                    mTouchMode = TOUCH_MODE_HSCROLL;
3998                    mViewStartX = distanceX;
3999                    initNextView(-mViewStartX);
4000                }
4001            } else {
4002                mTouchMode = TOUCH_MODE_VSCROLL;
4003            }
4004        } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
4005            // We are already scrolling horizontally, so check if we
4006            // changed the direction of scrolling so that the other week
4007            // is now visible.
4008            mViewStartX = distanceX;
4009            if (distanceX != 0) {
4010                int direction = (distanceX > 0) ? 1 : -1;
4011                if (direction != mPreviousDirection) {
4012                    // The user has switched the direction of scrolling
4013                    // so re-init the next view
4014                    initNextView(-mViewStartX);
4015                    mPreviousDirection = direction;
4016                }
4017            }
4018        }
4019
4020        if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
4021            // Calculate the top of the visible region in the calendar grid.
4022            // Increasing/decrease this will scroll the calendar grid up/down.
4023            mViewStartY = (int) ((mGestureCenterHour * (mCellHeight + DAY_GAP))
4024                    - focusY + DAY_HEADER_HEIGHT + mAlldayHeight);
4025
4026            // If dragging while already at the end, do a glow
4027            final int pulledToY = (int) (mScrollStartY + deltaY);
4028            if (pulledToY < 0) {
4029                mEdgeEffectTop.onPull(deltaY / mViewHeight);
4030                if (!mEdgeEffectBottom.isFinished()) {
4031                    mEdgeEffectBottom.onRelease();
4032                }
4033            } else if (pulledToY > mMaxViewStartY) {
4034                mEdgeEffectBottom.onPull(deltaY / mViewHeight);
4035                if (!mEdgeEffectTop.isFinished()) {
4036                    mEdgeEffectTop.onRelease();
4037                }
4038            }
4039
4040            if (mViewStartY < 0) {
4041                mViewStartY = 0;
4042                mRecalCenterHour = true;
4043            } else if (mViewStartY > mMaxViewStartY) {
4044                mViewStartY = mMaxViewStartY;
4045                mRecalCenterHour = true;
4046            }
4047            if (mRecalCenterHour) {
4048                // Calculate the hour that correspond to the average of the Y touch points
4049                mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight)
4050                        / (mCellHeight + DAY_GAP);
4051                mRecalCenterHour = false;
4052            }
4053            computeFirstHour();
4054        }
4055
4056        mScrolling = true;
4057
4058        mSelectionMode = SELECTION_HIDDEN;
4059        invalidate();
4060    }
4061
4062    private float getAverageY(MotionEvent me) {
4063        int count = me.getPointerCount();
4064        float focusY = 0;
4065        for (int i = 0; i < count; i++) {
4066            focusY += me.getY(i);
4067        }
4068        focusY /= count;
4069        return focusY;
4070    }
4071
4072    private void cancelAnimation() {
4073        Animation in = mViewSwitcher.getInAnimation();
4074        if (in != null) {
4075            // cancel() doesn't terminate cleanly.
4076            in.scaleCurrentDuration(0);
4077        }
4078        Animation out = mViewSwitcher.getOutAnimation();
4079        if (out != null) {
4080            // cancel() doesn't terminate cleanly.
4081            out.scaleCurrentDuration(0);
4082        }
4083    }
4084
4085    private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
4086        cancelAnimation();
4087
4088        mSelectionMode = SELECTION_HIDDEN;
4089        eventClickCleanup();
4090
4091        mOnFlingCalled = true;
4092
4093        if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
4094            // Horizontal fling.
4095            // initNextView(deltaX);
4096            mTouchMode = TOUCH_MODE_INITIAL_STATE;
4097            if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX);
4098            int deltaX = (int) e2.getX() - (int) e1.getX();
4099            switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX);
4100            mViewStartX = 0;
4101            return;
4102        }
4103
4104        if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) {
4105            if (DEBUG) Log.d(TAG, "doFling: no fling");
4106            return;
4107        }
4108
4109        // Vertical fling.
4110        mTouchMode = TOUCH_MODE_INITIAL_STATE;
4111        mViewStartX = 0;
4112
4113        if (DEBUG) {
4114            Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY);
4115        }
4116
4117        // Continue scrolling vertically
4118        mScrolling = true;
4119        mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */,
4120                (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */,
4121                mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE);
4122
4123        // When flinging down, show a glow when it hits the end only if it
4124        // wasn't started at the top
4125        if (velocityY > 0 && mViewStartY != 0) {
4126            mCallEdgeEffectOnAbsorb = true;
4127        }
4128        // When flinging up, show a glow when it hits the end only if it wasn't
4129        // started at the bottom
4130        else if (velocityY < 0 && mViewStartY != mMaxViewStartY) {
4131            mCallEdgeEffectOnAbsorb = true;
4132        }
4133        mHandler.post(mContinueScroll);
4134    }
4135
4136    private boolean initNextView(int deltaX) {
4137        // Change the view to the previous day or week
4138        DayView view = (DayView) mViewSwitcher.getNextView();
4139        Time date = view.mBaseDate;
4140        date.set(mBaseDate);
4141        boolean switchForward;
4142        if (deltaX > 0) {
4143            date.monthDay -= mNumDays;
4144            view.setSelectedDay(mSelectionDay - mNumDays);
4145            switchForward = false;
4146        } else {
4147            date.monthDay += mNumDays;
4148            view.setSelectedDay(mSelectionDay + mNumDays);
4149            switchForward = true;
4150        }
4151        date.normalize(true /* ignore isDst */);
4152        initView(view);
4153        view.layout(getLeft(), getTop(), getRight(), getBottom());
4154        view.reloadEvents();
4155        return switchForward;
4156    }
4157
4158    // ScaleGestureDetector.OnScaleGestureListener
4159    public boolean onScaleBegin(ScaleGestureDetector detector) {
4160        mHandleActionUp = false;
4161        float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight;
4162        mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP);
4163
4164        mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY()));
4165        mCellHeightBeforeScaleGesture = mCellHeight;
4166
4167        if (DEBUG_SCALING) {
4168            float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP);
4169            Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour
4170                    + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY
4171                    + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY());
4172        }
4173
4174        return true;
4175    }
4176
4177    // ScaleGestureDetector.OnScaleGestureListener
4178    public boolean onScale(ScaleGestureDetector detector) {
4179        float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY()));
4180
4181        mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY);
4182
4183        if (mCellHeight < mMinCellHeight) {
4184            // If mStartingSpanY is too small, even a small increase in the
4185            // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT
4186            mStartingSpanY = spanY;
4187            mCellHeight = mMinCellHeight;
4188            mCellHeightBeforeScaleGesture = mMinCellHeight;
4189        } else if (mCellHeight > MAX_CELL_HEIGHT) {
4190            mStartingSpanY = spanY;
4191            mCellHeight = MAX_CELL_HEIGHT;
4192            mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT;
4193        }
4194
4195        int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight;
4196        mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels;
4197        mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight;
4198
4199        if (DEBUG_SCALING) {
4200            float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP);
4201            Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: "
4202                    + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:"
4203                    + mCellHeight + " SpanY:" + detector.getCurrentSpanY());
4204        }
4205
4206        if (mViewStartY < 0) {
4207            mViewStartY = 0;
4208            mGestureCenterHour = (mViewStartY + gestureCenterInPixels)
4209                    / (float) (mCellHeight + DAY_GAP);
4210        } else if (mViewStartY > mMaxViewStartY) {
4211            mViewStartY = mMaxViewStartY;
4212            mGestureCenterHour = (mViewStartY + gestureCenterInPixels)
4213                    / (float) (mCellHeight + DAY_GAP);
4214        }
4215        computeFirstHour();
4216
4217        mRemeasure = true;
4218        invalidate();
4219        return true;
4220    }
4221
4222    // ScaleGestureDetector.OnScaleGestureListener
4223    public void onScaleEnd(ScaleGestureDetector detector) {
4224        mScrollStartY = mViewStartY;
4225        mInitialScrollY = 0;
4226        mInitialScrollX = 0;
4227        mStartingSpanY = 0;
4228    }
4229
4230    @Override
4231    public boolean onTouchEvent(MotionEvent ev) {
4232        int action = ev.getAction();
4233        if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount());
4234
4235        if ((ev.getActionMasked() == MotionEvent.ACTION_DOWN) ||
4236                (ev.getActionMasked() == MotionEvent.ACTION_UP) ||
4237                (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) ||
4238                (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN)) {
4239            mRecalCenterHour = true;
4240        }
4241
4242        if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) {
4243            mScaleGestureDetector.onTouchEvent(ev);
4244        }
4245
4246        switch (action) {
4247            case MotionEvent.ACTION_DOWN:
4248                mStartingScroll = true;
4249                if (DEBUG) {
4250                    Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt="
4251                            + ev.getPointerCount());
4252                }
4253
4254                int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
4255                if (ev.getY() < bottom) {
4256                    mTouchStartedInAlldayArea = true;
4257                } else {
4258                    mTouchStartedInAlldayArea = false;
4259                }
4260                mHandleActionUp = true;
4261                mGestureDetector.onTouchEvent(ev);
4262                return true;
4263
4264            case MotionEvent.ACTION_MOVE:
4265                if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this);
4266                mGestureDetector.onTouchEvent(ev);
4267                return true;
4268
4269            case MotionEvent.ACTION_UP:
4270                if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp);
4271                mEdgeEffectTop.onRelease();
4272                mEdgeEffectBottom.onRelease();
4273                mStartingScroll = false;
4274                mGestureDetector.onTouchEvent(ev);
4275                if (!mHandleActionUp) {
4276                    mHandleActionUp = true;
4277                    mViewStartX = 0;
4278                    invalidate();
4279                    return true;
4280                }
4281
4282                if (mOnFlingCalled) {
4283                    return true;
4284                }
4285
4286                // If we were scrolling, then reset the selected hour so that it
4287                // is visible.
4288                if (mScrolling) {
4289                    mScrolling = false;
4290                    resetSelectedHour();
4291                    invalidate();
4292                }
4293
4294                if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
4295                    mTouchMode = TOUCH_MODE_INITIAL_STATE;
4296                    if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) {
4297                        // The user has gone beyond the threshold so switch views
4298                        if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views");
4299                        switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0);
4300                        mViewStartX = 0;
4301                        return true;
4302                    } else {
4303                        // Not beyond the threshold so invalidate which will cause
4304                        // the view to snap back. Also call recalc() to ensure
4305                        // that we have the correct starting date and title.
4306                        if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back");
4307                        recalc();
4308                        invalidate();
4309                        mViewStartX = 0;
4310                    }
4311                }
4312
4313                return true;
4314
4315                // This case isn't expected to happen.
4316            case MotionEvent.ACTION_CANCEL:
4317                if (DEBUG) Log.e(TAG, "ACTION_CANCEL");
4318                mGestureDetector.onTouchEvent(ev);
4319                mScrolling = false;
4320                resetSelectedHour();
4321                return true;
4322
4323            default:
4324                if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString());
4325                if (mGestureDetector.onTouchEvent(ev)) {
4326                    return true;
4327                }
4328                return super.onTouchEvent(ev);
4329        }
4330    }
4331
4332    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
4333        MenuItem item;
4334
4335        // If the trackball is held down, then the context menu pops up and
4336        // we never get onKeyUp() for the long-press. So check for it here
4337        // and change the selection to the long-press state.
4338        if (mSelectionMode != SELECTION_LONGPRESS) {
4339            mSelectionMode = SELECTION_LONGPRESS;
4340            invalidate();
4341        }
4342
4343        final long startMillis = getSelectedTimeInMillis();
4344        int flags = DateUtils.FORMAT_SHOW_TIME
4345                | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
4346                | DateUtils.FORMAT_SHOW_WEEKDAY;
4347        final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags);
4348        menu.setHeaderTitle(title);
4349
4350        int numSelectedEvents = mSelectedEvents.size();
4351        if (mNumDays == 1) {
4352            // Day view.
4353
4354            // If there is a selected event, then allow it to be viewed and
4355            // edited.
4356            if (numSelectedEvents >= 1) {
4357                item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view);
4358                item.setOnMenuItemClickListener(mContextMenuHandler);
4359                item.setIcon(android.R.drawable.ic_menu_info_details);
4360
4361                int accessLevel = getEventAccessLevel(mContext, mSelectedEvent);
4362                if (accessLevel == ACCESS_LEVEL_EDIT) {
4363                    item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit);
4364                    item.setOnMenuItemClickListener(mContextMenuHandler);
4365                    item.setIcon(android.R.drawable.ic_menu_edit);
4366                    item.setAlphabeticShortcut('e');
4367                }
4368
4369                if (accessLevel >= ACCESS_LEVEL_DELETE) {
4370                    item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete);
4371                    item.setOnMenuItemClickListener(mContextMenuHandler);
4372                    item.setIcon(android.R.drawable.ic_menu_delete);
4373                }
4374
4375                item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4376                item.setOnMenuItemClickListener(mContextMenuHandler);
4377                item.setIcon(android.R.drawable.ic_menu_add);
4378                item.setAlphabeticShortcut('n');
4379            } else {
4380                // Otherwise, if the user long-pressed on a blank hour, allow
4381                // them to create an event. They can also do this by tapping.
4382                item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4383                item.setOnMenuItemClickListener(mContextMenuHandler);
4384                item.setIcon(android.R.drawable.ic_menu_add);
4385                item.setAlphabeticShortcut('n');
4386            }
4387        } else {
4388            // Week view.
4389
4390            // If there is a selected event, then allow it to be viewed and
4391            // edited.
4392            if (numSelectedEvents >= 1) {
4393                item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view);
4394                item.setOnMenuItemClickListener(mContextMenuHandler);
4395                item.setIcon(android.R.drawable.ic_menu_info_details);
4396
4397                int accessLevel = getEventAccessLevel(mContext, mSelectedEvent);
4398                if (accessLevel == ACCESS_LEVEL_EDIT) {
4399                    item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit);
4400                    item.setOnMenuItemClickListener(mContextMenuHandler);
4401                    item.setIcon(android.R.drawable.ic_menu_edit);
4402                    item.setAlphabeticShortcut('e');
4403                }
4404
4405                if (accessLevel >= ACCESS_LEVEL_DELETE) {
4406                    item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete);
4407                    item.setOnMenuItemClickListener(mContextMenuHandler);
4408                    item.setIcon(android.R.drawable.ic_menu_delete);
4409                }
4410            }
4411
4412            item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create);
4413            item.setOnMenuItemClickListener(mContextMenuHandler);
4414            item.setIcon(android.R.drawable.ic_menu_add);
4415            item.setAlphabeticShortcut('n');
4416
4417            item = menu.add(0, MENU_DAY, 0, R.string.show_day_view);
4418            item.setOnMenuItemClickListener(mContextMenuHandler);
4419            item.setIcon(android.R.drawable.ic_menu_day);
4420            item.setAlphabeticShortcut('d');
4421        }
4422
4423        mPopup.dismiss();
4424    }
4425
4426    private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
4427        public boolean onMenuItemClick(MenuItem item) {
4428            switch (item.getItemId()) {
4429                case MENU_EVENT_VIEW: {
4430                    if (mSelectedEvent != null) {
4431                        mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS,
4432                                mSelectedEvent.id, mSelectedEvent.startMillis,
4433                                mSelectedEvent.endMillis, 0, 0, -1);
4434                    }
4435                    break;
4436                }
4437                case MENU_EVENT_EDIT: {
4438                    if (mSelectedEvent != null) {
4439                        mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT,
4440                                mSelectedEvent.id, mSelectedEvent.startMillis,
4441                                mSelectedEvent.endMillis, 0, 0, -1);
4442                    }
4443                    break;
4444                }
4445                case MENU_DAY: {
4446                    mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1,
4447                            ViewType.DAY);
4448                    break;
4449                }
4450                case MENU_AGENDA: {
4451                    mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1,
4452                            ViewType.AGENDA);
4453                    break;
4454                }
4455                case MENU_EVENT_CREATE: {
4456                    long startMillis = getSelectedTimeInMillis();
4457                    long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
4458                    mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1,
4459                            startMillis, endMillis, 0, 0, -1);
4460                    break;
4461                }
4462                case MENU_EVENT_DELETE: {
4463                    if (mSelectedEvent != null) {
4464                        Event selectedEvent = mSelectedEvent;
4465                        long begin = selectedEvent.startMillis;
4466                        long end = selectedEvent.endMillis;
4467                        long id = selectedEvent.id;
4468                        mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin,
4469                                end, 0, 0, -1);
4470                    }
4471                    break;
4472                }
4473                default: {
4474                    return false;
4475                }
4476            }
4477            return true;
4478        }
4479    }
4480
4481    private static int getEventAccessLevel(Context context, Event e) {
4482        ContentResolver cr = context.getContentResolver();
4483
4484        int accessLevel = Calendars.CAL_ACCESS_NONE;
4485
4486        // Get the calendar id for this event
4487        Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
4488                new String[] { Events.CALENDAR_ID },
4489                null /* selection */,
4490                null /* selectionArgs */,
4491                null /* sort */);
4492
4493        if (cursor == null) {
4494            return ACCESS_LEVEL_NONE;
4495        }
4496
4497        if (cursor.getCount() == 0) {
4498            cursor.close();
4499            return ACCESS_LEVEL_NONE;
4500        }
4501
4502        cursor.moveToFirst();
4503        long calId = cursor.getLong(0);
4504        cursor.close();
4505
4506        Uri uri = Calendars.CONTENT_URI;
4507        String where = String.format(CALENDARS_WHERE, calId);
4508        cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
4509
4510        String calendarOwnerAccount = null;
4511        if (cursor != null) {
4512            cursor.moveToFirst();
4513            accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
4514            calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
4515            cursor.close();
4516        }
4517
4518        if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) {
4519            return ACCESS_LEVEL_NONE;
4520        }
4521
4522        if (e.guestsCanModify) {
4523            return ACCESS_LEVEL_EDIT;
4524        }
4525
4526        if (!TextUtils.isEmpty(calendarOwnerAccount)
4527                && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) {
4528            return ACCESS_LEVEL_EDIT;
4529        }
4530
4531        return ACCESS_LEVEL_DELETE;
4532    }
4533
4534    /**
4535     * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
4536     * If the touch position is not within the displayed grid, then this
4537     * method returns false.
4538     *
4539     * @param x the x position of the touch
4540     * @param y the y position of the touch
4541     * @param keepOldSelection - do not change the selection info (used for invoking accessibility
4542     *                           messages)
4543     * @return true if the touch position is valid
4544     */
4545    private boolean setSelectionFromPosition(int x, final int y, boolean keepOldSelection) {
4546
4547        Event savedEvent = null;
4548        int savedDay = 0;
4549        int savedHour = 0;
4550        boolean savedAllDay = false;
4551        if (keepOldSelection) {
4552            // Store selection info and restore it at the end. This way, we can invoke the
4553            // right accessibility message without affecting the selection.
4554            savedEvent = mSelectedEvent;
4555            savedDay = mSelectionDay;
4556            savedHour = mSelectionHour;
4557            savedAllDay = mSelectionAllday;
4558        }
4559        if (x < mHoursWidth) {
4560            x = mHoursWidth;
4561        }
4562
4563        int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
4564        if (day >= mNumDays) {
4565            day = mNumDays - 1;
4566        }
4567        day += mFirstJulianDay;
4568        setSelectedDay(day);
4569
4570        if (y < DAY_HEADER_HEIGHT) {
4571            sendAccessibilityEventAsNeeded(false);
4572            return false;
4573        }
4574
4575        setSelectedHour(mFirstHour); /* First fully visible hour */
4576
4577        if (y < mFirstCell) {
4578            mSelectionAllday = true;
4579        } else {
4580            // y is now offset from top of the scrollable region
4581            int adjustedY = y - mFirstCell;
4582
4583            if (adjustedY < mFirstHourOffset) {
4584                setSelectedHour(mSelectionHour - 1); /* In the partially visible hour */
4585            } else {
4586                setSelectedHour(mSelectionHour +
4587                        (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP));
4588            }
4589
4590            mSelectionAllday = false;
4591        }
4592
4593        findSelectedEvent(x, y);
4594
4595//        Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: "
4596//                + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: "
4597//                + mFirstHourOffset);
4598//        if (mSelectedEvent != null) {
4599//            Log.i("Cal", "  num events: " + mSelectedEvents.size() + " event: "
4600//                    + mSelectedEvent.title);
4601//            for (Event ev : mSelectedEvents) {
4602//                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
4603//                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
4604//                String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags);
4605//
4606//                Log.i("Cal", "  " + timeRange + " " + ev.title);
4607//            }
4608//        }
4609        sendAccessibilityEventAsNeeded(true);
4610
4611        // Restore old values
4612        if (keepOldSelection) {
4613            mSelectedEvent = savedEvent;
4614            mSelectionDay = savedDay;
4615            mSelectionHour = savedHour;
4616            mSelectionAllday = savedAllDay;
4617        }
4618        return true;
4619    }
4620
4621    private void findSelectedEvent(int x, int y) {
4622        int date = mSelectionDay;
4623        int cellWidth = mCellWidth;
4624        ArrayList<Event> events = mEvents;
4625        int numEvents = events.size();
4626        int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay);
4627        int top = 0;
4628        setSelectedEvent(null);
4629
4630        mSelectedEvents.clear();
4631        if (mSelectionAllday) {
4632            float yDistance;
4633            float minYdistance = 10000.0f; // any large number
4634            Event closestEvent = null;
4635            float drawHeight = mAlldayHeight;
4636            int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
4637            int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount;
4638            if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
4639                // Leave a gap for the 'box +n' text
4640                maxUnexpandedColumn--;
4641            }
4642            events = mAllDayEvents;
4643            numEvents = events.size();
4644            for (int i = 0; i < numEvents; i++) {
4645                Event event = events.get(i);
4646                if (!event.drawAsAllday() ||
4647                        (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) {
4648                    // Don't check non-allday events or events that aren't shown
4649                    continue;
4650                }
4651
4652                if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
4653                    float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents
4654                            : mMaxUnexpandedAlldayEventCount;
4655                    float height = drawHeight / numRectangles;
4656                    if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
4657                        height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
4658                    }
4659                    float eventTop = yOffset + height * event.getColumn();
4660                    float eventBottom = eventTop + height;
4661                    if (eventTop < y && eventBottom > y) {
4662                        // If the touch is inside the event rectangle, then
4663                        // add the event.
4664                        mSelectedEvents.add(event);
4665                        closestEvent = event;
4666                        break;
4667                    } else {
4668                        // Find the closest event
4669                        if (eventTop >= y) {
4670                            yDistance = eventTop - y;
4671                        } else {
4672                            yDistance = y - eventBottom;
4673                        }
4674                        if (yDistance < minYdistance) {
4675                            minYdistance = yDistance;
4676                            closestEvent = event;
4677                        }
4678                    }
4679                }
4680            }
4681            setSelectedEvent(closestEvent);
4682            return;
4683        }
4684
4685        // Adjust y for the scrollable bitmap
4686        y += mViewStartY - mFirstCell;
4687
4688        // Use a region around (x,y) for the selection region
4689        Rect region = mRect;
4690        region.left = x - 10;
4691        region.right = x + 10;
4692        region.top = y - 10;
4693        region.bottom = y + 10;
4694
4695        EventGeometry geometry = mEventGeometry;
4696
4697        for (int i = 0; i < numEvents; i++) {
4698            Event event = events.get(i);
4699            // Compute the event rectangle.
4700            if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
4701                continue;
4702            }
4703
4704            // If the event intersects the selection region, then add it to
4705            // mSelectedEvents.
4706            if (geometry.eventIntersectsSelection(event, region)) {
4707                mSelectedEvents.add(event);
4708            }
4709        }
4710
4711        // If there are any events in the selected region, then assign the
4712        // closest one to mSelectedEvent.
4713        if (mSelectedEvents.size() > 0) {
4714            int len = mSelectedEvents.size();
4715            Event closestEvent = null;
4716            float minDist = mViewWidth + mViewHeight; // some large distance
4717            for (int index = 0; index < len; index++) {
4718                Event ev = mSelectedEvents.get(index);
4719                float dist = geometry.pointToEvent(x, y, ev);
4720                if (dist < minDist) {
4721                    minDist = dist;
4722                    closestEvent = ev;
4723                }
4724            }
4725            setSelectedEvent(closestEvent);
4726
4727            // Keep the selected hour and day consistent with the selected
4728            // event. They could be different if we touched on an empty hour
4729            // slot very close to an event in the previous hour slot. In
4730            // that case we will select the nearby event.
4731            int startDay = mSelectedEvent.startDay;
4732            int endDay = mSelectedEvent.endDay;
4733            if (mSelectionDay < startDay) {
4734                setSelectedDay(startDay);
4735            } else if (mSelectionDay > endDay) {
4736                setSelectedDay(endDay);
4737            }
4738
4739            int startHour = mSelectedEvent.startTime / 60;
4740            int endHour;
4741            if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
4742                endHour = (mSelectedEvent.endTime - 1) / 60;
4743            } else {
4744                endHour = mSelectedEvent.endTime / 60;
4745            }
4746
4747            if (mSelectionHour < startHour && mSelectionDay == startDay) {
4748                setSelectedHour(startHour);
4749            } else if (mSelectionHour > endHour && mSelectionDay == endDay) {
4750                setSelectedHour(endHour);
4751            }
4752        }
4753    }
4754
4755    // Encapsulates the code to continue the scrolling after the
4756    // finger is lifted. Instead of stopping the scroll immediately,
4757    // the scroll continues to "free spin" and gradually slows down.
4758    private class ContinueScroll implements Runnable {
4759        public void run() {
4760            mScrolling = mScrolling && mScroller.computeScrollOffset();
4761            if (!mScrolling || mPaused) {
4762                resetSelectedHour();
4763                invalidate();
4764                return;
4765            }
4766
4767            mViewStartY = mScroller.getCurrY();
4768
4769            if (mCallEdgeEffectOnAbsorb) {
4770                if (mViewStartY < 0) {
4771                    mEdgeEffectTop.onAbsorb((int) mLastVelocity);
4772                    mCallEdgeEffectOnAbsorb = false;
4773                } else if (mViewStartY > mMaxViewStartY) {
4774                    mEdgeEffectBottom.onAbsorb((int) mLastVelocity);
4775                    mCallEdgeEffectOnAbsorb = false;
4776                }
4777                mLastVelocity = mScroller.getCurrVelocity();
4778            }
4779
4780            if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) {
4781                // Allow overscroll/springback only on a fling,
4782                // not a pull/fling from the end
4783                if (mViewStartY < 0) {
4784                    mViewStartY = 0;
4785                } else if (mViewStartY > mMaxViewStartY) {
4786                    mViewStartY = mMaxViewStartY;
4787                }
4788            }
4789
4790            computeFirstHour();
4791            mHandler.post(this);
4792            invalidate();
4793        }
4794    }
4795
4796    /**
4797     * Cleanup the pop-up and timers.
4798     */
4799    public void cleanup() {
4800        // Protect against null-pointer exceptions
4801        if (mPopup != null) {
4802            mPopup.dismiss();
4803        }
4804        mPaused = true;
4805        mLastPopupEventID = INVALID_EVENT_ID;
4806        if (mHandler != null) {
4807            mHandler.removeCallbacks(mDismissPopup);
4808            mHandler.removeCallbacks(mUpdateCurrentTime);
4809        }
4810
4811        Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT,
4812            mCellHeight);
4813        // Clear all click animations
4814        eventClickCleanup();
4815        // Turn off redraw
4816        mRemeasure = false;
4817        // Turn off scrolling to make sure the view is in the correct state if we fling back to it
4818        mScrolling = false;
4819    }
4820
4821    private void eventClickCleanup() {
4822        this.removeCallbacks(mClearClick);
4823        this.removeCallbacks(mSetClick);
4824        mClickedEvent = null;
4825        mSavedClickedEvent = null;
4826    }
4827
4828    private void setSelectedEvent(Event e) {
4829        mSelectedEvent = e;
4830        mSelectedEventForAccessibility = e;
4831    }
4832
4833    private void setSelectedHour(int h) {
4834        mSelectionHour = h;
4835        mSelectionHourForAccessibility = h;
4836    }
4837    private void setSelectedDay(int d) {
4838        mSelectionDay = d;
4839        mSelectionDayForAccessibility = d;
4840    }
4841
4842    /**
4843     * Restart the update timer
4844     */
4845    public void restartCurrentTimeUpdates() {
4846        mPaused = false;
4847        if (mHandler != null) {
4848            mHandler.removeCallbacks(mUpdateCurrentTime);
4849            mHandler.post(mUpdateCurrentTime);
4850        }
4851    }
4852
4853    @Override
4854    protected void onDetachedFromWindow() {
4855        cleanup();
4856        super.onDetachedFromWindow();
4857    }
4858
4859    class DismissPopup implements Runnable {
4860        public void run() {
4861            // Protect against null-pointer exceptions
4862            if (mPopup != null) {
4863                mPopup.dismiss();
4864            }
4865        }
4866    }
4867
4868    class UpdateCurrentTime implements Runnable {
4869        public void run() {
4870            long currentTime = System.currentTimeMillis();
4871            mCurrentTime.set(currentTime);
4872            //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
4873            if (!DayView.this.mPaused) {
4874                mHandler.postDelayed(mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY
4875                        - (currentTime % UPDATE_CURRENT_TIME_DELAY));
4876            }
4877            mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
4878            invalidate();
4879        }
4880    }
4881
4882    class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
4883        @Override
4884        public boolean onSingleTapUp(MotionEvent ev) {
4885            if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp");
4886            DayView.this.doSingleTapUp(ev);
4887            return true;
4888        }
4889
4890        @Override
4891        public void onLongPress(MotionEvent ev) {
4892            if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress");
4893            DayView.this.doLongPress(ev);
4894        }
4895
4896        @Override
4897        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
4898            if (DEBUG) Log.e(TAG, "GestureDetector.onScroll");
4899            eventClickCleanup();
4900            if (mTouchStartedInAlldayArea) {
4901                if (Math.abs(distanceX) < Math.abs(distanceY)) {
4902                    // Make sure that click feedback is gone when you scroll from the
4903                    // all day area
4904                    invalidate();
4905                    return false;
4906                }
4907                // don't scroll vertically if this started in the allday area
4908                distanceY = 0;
4909            }
4910            DayView.this.doScroll(e1, e2, distanceX, distanceY);
4911            return true;
4912        }
4913
4914        @Override
4915        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
4916            if (DEBUG) Log.e(TAG, "GestureDetector.onFling");
4917
4918            if (mTouchStartedInAlldayArea) {
4919                if (Math.abs(velocityX) < Math.abs(velocityY)) {
4920                    return false;
4921                }
4922                // don't fling vertically if this started in the allday area
4923                velocityY = 0;
4924            }
4925            DayView.this.doFling(e1, e2, velocityX, velocityY);
4926            return true;
4927        }
4928
4929        @Override
4930        public boolean onDown(MotionEvent ev) {
4931            if (DEBUG) Log.e(TAG, "GestureDetector.onDown");
4932            DayView.this.doDown(ev);
4933            return true;
4934        }
4935    }
4936
4937    @Override
4938    public boolean onLongClick(View v) {
4939        int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
4940        long time = getSelectedTimeInMillis();
4941        if (!mSelectionAllday) {
4942            flags |= DateUtils.FORMAT_SHOW_TIME;
4943        }
4944        if (DateFormat.is24HourFormat(mContext)) {
4945            flags |= DateUtils.FORMAT_24HOUR;
4946        }
4947        mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags);
4948        new AlertDialog.Builder(mContext).setTitle(mLongPressTitle)
4949                .setItems(mLongPressItems, new DialogInterface.OnClickListener() {
4950                    @Override
4951                    public void onClick(DialogInterface dialog, int which) {
4952                        if (which == 0) {
4953                            long extraLong = 0;
4954                            if (mSelectionAllday) {
4955                                extraLong = CalendarController.EXTRA_CREATE_ALL_DAY;
4956                            }
4957                            mController.sendEventRelatedEventWithExtra(this,
4958                                    EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1,
4959                                    -1, extraLong, -1);
4960                        }
4961                    }
4962                }).show().setCanceledOnTouchOutside(true);
4963        return true;
4964    }
4965
4966    // The rest of this file was borrowed from Launcher2 - PagedView.java
4967    private static final int MINIMUM_SNAP_VELOCITY = 2200;
4968
4969    private class ScrollInterpolator implements Interpolator {
4970        public ScrollInterpolator() {
4971        }
4972
4973        public float getInterpolation(float t) {
4974            t -= 1.0f;
4975            t = t * t * t * t * t + 1;
4976
4977            if ((1 - t) * mAnimationDistance < 1) {
4978                cancelAnimation();
4979            }
4980
4981            return t;
4982        }
4983    }
4984
4985    private long calculateDuration(float delta, float width, float velocity) {
4986        /*
4987         * Here we compute a "distance" that will be used in the computation of
4988         * the overall snap duration. This is a function of the actual distance
4989         * that needs to be traveled; we keep this value close to half screen
4990         * size in order to reduce the variance in snap duration as a function
4991         * of the distance the page needs to travel.
4992         */
4993        final float halfScreenSize = width / 2;
4994        float distanceRatio = delta / width;
4995        float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio);
4996        float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration;
4997
4998        velocity = Math.abs(velocity);
4999        velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity);
5000
5001        /*
5002         * we want the page's snap velocity to approximately match the velocity
5003         * at which the user flings, so we scale the duration by a value near to
5004         * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to
5005         * make it a little slower.
5006         */
5007        long duration = 6 * Math.round(1000 * Math.abs(distance / velocity));
5008        if (DEBUG) {
5009            Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:"
5010                    + distanceRatio + " distance:" + distance + " velocity:" + velocity
5011                    + " duration:" + duration + " distanceInfluenceForSnapDuration:"
5012                    + distanceInfluenceForSnapDuration);
5013        }
5014        return duration;
5015    }
5016
5017    /*
5018     * We want the duration of the page snap animation to be influenced by the
5019     * distance that the screen has to travel, however, we don't want this
5020     * duration to be effected in a purely linear fashion. Instead, we use this
5021     * method to moderate the effect that the distance of travel has on the
5022     * overall snap duration.
5023     */
5024    private float distanceInfluenceForSnapDuration(float f) {
5025        f -= 0.5f; // center the values about 0.
5026        f *= 0.3f * Math.PI / 2.0f;
5027        return (float) Math.sin(f);
5028    }
5029}
5030