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