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