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