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