DayView.java revision b5e3c17201b2c08ca9f3fd33f4585e1ec8fa71b1
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.Iterator;
91import java.util.Locale;
92import java.util.regex.Matcher;
93import java.util.regex.Pattern;
94
95/**
96 * View for multi-day view. So far only 1 and 7 day have been tested.
97 */
98public class DayView extends View implements View.OnCreateContextMenuListener,
99        ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener
100        {
101    private static String TAG = "DayView";
102    private static boolean DEBUG = false;
103    private static final String HOUR_FORMAT_12H = "%A %I%p";
104    private static final String HOUR_FORMAT_24H = "%A %H";
105    private static final String PERIOD_SPACE = ". ";
106
107    private static float mScale = 0; // Used for supporting different screen densities
108    private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
109    // Duration of the allday expansion
110    private static final long ANIMATION_DURATION = 400;
111    // duration of the more allday event text fade
112    private static final long ANIMATION_SECONDARY_DURATION = 200;
113
114    private static final int MENU_AGENDA = 2;
115    private static final int MENU_DAY = 3;
116    private static final int MENU_EVENT_VIEW = 5;
117    private static final int MENU_EVENT_CREATE = 6;
118    private static final int MENU_EVENT_EDIT = 7;
119    private static final int MENU_EVENT_DELETE = 8;
120
121    private static int DEFAULT_CELL_HEIGHT = 64;
122    private static int MAX_CELL_HEIGHT = 150;
123    private static int MIN_Y_SPAN = 100;
124
125    private boolean mOnFlingCalled;
126    private boolean mStartingScroll = false;
127    /**
128     * ID of the last event which was displayed with the toast popup.
129     *
130     * This is used to prevent popping up multiple quick views for the same event, especially
131     * during calendar syncs. This becomes valid when an event is selected, either by default
132     * on starting calendar or by scrolling to an event. It becomes invalid when the user
133     * explicitly scrolls to an empty time slot, changes views, or deletes the event.
134     */
135    private long mLastPopupEventID;
136
137    protected Context mContext;
138
139    private static final String[] CALENDARS_PROJECTION = new String[] {
140        Calendars._ID,          // 0
141        Calendars.CALENDAR_ACCESS_LEVEL, // 1
142        Calendars.OWNER_ACCOUNT, // 2
143    };
144    private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
145    private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
146    private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
147
148    private static final String[] ATTENDEES_PROJECTION = new String[] {
149        Attendees._ID,                      // 0
150        Attendees.ATTENDEE_RELATIONSHIP,    // 1
151    };
152    private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
153    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
154
155    private static final int FROM_NONE = 0;
156    private static final int FROM_ABOVE = 1;
157    private static final int FROM_BELOW = 2;
158    private static final int FROM_LEFT = 4;
159    private static final int FROM_RIGHT = 8;
160
161    private static final int ACCESS_LEVEL_NONE = 0;
162    private static final int ACCESS_LEVEL_DELETE = 1;
163    private static final int ACCESS_LEVEL_EDIT = 2;
164
165    private static int mHorizontalSnapBackThreshold = 128;
166
167    private ContinueScroll mContinueScroll = new ContinueScroll();
168
169    // Make this visible within the package for more informative debugging
170    Time mBaseDate;
171    private Time mCurrentTime;
172    //Update the current time line every five minutes if the window is left open that long
173    private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
174    private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
175    private int mTodayJulianDay;
176
177    private Typeface mBold = Typeface.DEFAULT_BOLD;
178    private int mFirstJulianDay;
179    private int mLastJulianDay;
180
181    private int mMonthLength;
182    private int mFirstVisibleDate;
183    private int mFirstVisibleDayOfWeek;
184    private int[] mEarliestStartHour;    // indexed by the week day offset
185    private boolean[] mHasAllDayEvent;   // indexed by the week day offset
186    private String mAllDayString;
187    private String mEventCountTemplate;
188
189    protected static StringBuilder mStringBuilder = new StringBuilder(50);
190    // TODO recreate formatter when locale changes
191    protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
192
193    private Runnable mTZUpdater = new Runnable() {
194        @Override
195        public void run() {
196            String tz = Utils.getTimeZone(mContext, this);
197            mBaseDate.timezone = tz;
198            mBaseDate.normalize(true);
199            mCurrentTime.switchTimezone(tz);
200            invalidate();
201        }
202    };
203
204    AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
205        @Override
206        public void onAnimationStart(Animator animation) {
207            mScrolling = true;
208        }
209
210        @Override
211        public void onAnimationCancel(Animator animation) {
212            mScrolling = false;
213        }
214
215        @Override
216        public void onAnimationEnd(Animator animation) {
217            mScrolling = false;
218            resetSelectedHour();
219            invalidate();
220        }
221    };
222
223    /**
224     * This variable helps to avoid unnecessarily reloading events by keeping
225     * track of the start millis parameter used for the most recent loading
226     * of events.  If the next reload matches this, then the events are not
227     * reloaded.  To force a reload, set this to zero (this is set to zero
228     * in the method clearCachedEvents()).
229     */
230    private long mLastReloadMillis;
231
232    private ArrayList<Event> mEvents = new ArrayList<Event>();
233    private ArrayList<Event> mAllDayEvents = new ArrayList<Event>();
234    private StaticLayout[] mLayouts = null;
235    private StaticLayout[] mAllDayLayouts = null;
236    private StaticLayout mAllDayTextLayout = null;
237    private int mSelectionDay;        // Julian day
238    private int mSelectionHour;
239
240    boolean mSelectionAllday;
241
242    /** Width of a day or non-conflicting event */
243    private int mCellWidth;
244
245    // Pre-allocate these objects and re-use them
246    private Rect mRect = new Rect();
247    private Rect mDestRect = new Rect();
248    private Rect mSelectionRect = new Rect();
249    // This encloses the more allDay events icon
250    private Rect mExpandAllDayRect = new Rect();
251    // TODO Clean up paint usage
252    private Paint mPaint = new Paint();
253    private Paint mEventTextPaint = new Paint();
254    private Paint mSelectionPaint = new Paint();
255    private float[] mLines;
256
257    private int mFirstDayOfWeek; // First day of the week
258
259    private PopupWindow mPopup;
260    private View mPopupView;
261
262    // The number of milliseconds to show the popup window
263    private static final int POPUP_DISMISS_DELAY = 3000;
264    private DismissPopup mDismissPopup = new DismissPopup();
265
266    private boolean mRemeasure = true;
267
268    private final EventLoader mEventLoader;
269    protected final EventGeometry mEventGeometry;
270
271    private static float GRID_LINE_LEFT_MARGIN = 0;
272    private static final float GRID_LINE_INNER_WIDTH = 1;
273
274    private static final int DAY_GAP = 1;
275    private static final int HOUR_GAP = 1;
276    // This is the standard height of an allday event with no restrictions
277    private static int SINGLE_ALLDAY_HEIGHT = 34;
278    /**
279    * This is the minimum desired height of a allday event.
280    * When unexpanded, allday events will use this height.
281    * When expanded allDay events will attempt to grow to fit all
282    * events at this height.
283    */
284    private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels
285    /**
286     * This is how big the unexpanded allday height is allowed to be.
287     * It will get adjusted based on screen size
288     */
289    private static int MAX_UNEXPANDED_ALLDAY_HEIGHT =
290            (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4);
291    /**
292     * This is the minimum size reserved for displaying regular events.
293     * The expanded allDay region can't expand into this.
294     */
295    private static int MIN_HOURS_HEIGHT = 180;
296    private static int ALLDAY_TOP_MARGIN = 1;
297    // The largest a single allDay event will become.
298    private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34;
299
300    private static int HOURS_TOP_MARGIN = 2;
301    private static int HOURS_LEFT_MARGIN = 2;
302    private static int HOURS_RIGHT_MARGIN = 4;
303    private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
304
305    private static int CURRENT_TIME_LINE_HEIGHT = 2;
306    private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
307    private static final int CURRENT_TIME_LINE_SIDE_BUFFER = 2;
308
309    /* package */ static final int MINUTES_PER_HOUR = 60;
310    /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
311    /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
312    /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
313    /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
314
315    private static final int DECLINED_ALPHA = 0x66000000;
316    private static final int DATE_HEADER_ALPHA = 0x26000000;
317    private static final int DATE_HEADER_TODAY_ALPHA = 0x99000000;
318    // More events text will transition between invisible and this alpha
319    private static final int MORE_EVENTS_MAX_ALPHA = 0x4C;
320    private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0;
321    private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5;
322    private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6;
323    private static int DAY_HEADER_LEFT_MARGIN = 5;
324    private static int DAY_HEADER_RIGHT_MARGIN = 4;
325    private static int DAY_HEADER_BOTTOM_MARGIN = 3;
326    private static float DAY_HEADER_FONT_SIZE = 14;
327    private static float DATE_HEADER_FONT_SIZE = 32;
328    private static float NORMAL_FONT_SIZE = 12;
329    private static float EVENT_TEXT_FONT_SIZE = 12;
330    private static float HOURS_TEXT_SIZE = 12;
331    private static float ALLDAY_TEXT_SIZE = 12;
332    private static float AMPM_TEXT_SIZE = 9;
333    private static int MIN_HOURS_WIDTH = 96;
334    private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
335    private static final int MAX_EVENT_TEXT_LEN = 500;
336    // smallest height to draw an event with
337    private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels
338    private static int CALENDAR_COLOR_SQUARE_SIZE = 10;
339    private static int EVENT_RECT_TOP_MARGIN = 1;
340    private static int EVENT_RECT_BOTTOM_MARGIN = 1;
341    private static int EVENT_RECT_LEFT_MARGIN = 2;
342    private static int EVENT_RECT_RIGHT_MARGIN = 1;
343    private static int EVENT_RECT_STROKE_WIDTH = 2;
344    private static int EVENT_TEXT_TOP_MARGIN = 2;
345    private static int EVENT_TEXT_BOTTOM_MARGIN = 2;
346    private static int EVENT_TEXT_LEFT_MARGIN = 2;
347    private static int EVENT_TEXT_RIGHT_MARGIN = 2;
348    private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1;
349    private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN;
350    private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN;
351    private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN;
352    private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN;
353    // margins and sizing for the expand allday icon
354    private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10;
355    // sizing for "box +n" in allDay events
356    private static int EVENT_SQUARE_WIDTH = 10;
357    private static int EVENT_LINE_PADDING = 4;
358
359    private static int mPressedColor;
360    private static int mEventTextColor;
361    private static int mMoreEventsTextColor;
362
363    private static int mWeek_saturdayColor;
364    private static int mWeek_sundayColor;
365    private static int mCalendarDateBannerTextColor;
366    private static int mCalendarAmPmLabel;
367    private static int mCalendarGridAreaSelected;
368    private static int mCalendarGridLineInnerHorizontalColor;
369    private static int mCalendarGridLineInnerVerticalColor;
370    private static int mTodayBgColor;
371    private static int mBgColor;
372    private static int mCalendarHourLabelColor;
373    private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA;
374
375    private float mAnimationDistance = 0;
376    private int mViewStartX;
377    private int mViewStartY;
378    private int mMaxViewStartY;
379    private int mViewHeight;
380    private int mViewWidth;
381    private int mGridAreaHeight = -1;
382    private static int mCellHeight = 0; // shared among all DayViews
383    private static int mMinCellHeight = 32;
384    private int mScrollStartY;
385    private int mPreviousDirection;
386
387    /**
388     * Vertical distance or span between the two touch points at the start of a
389     * scaling gesture
390     */
391    private float mStartingSpanY = 0;
392    /** Height of 1 hour in pixels at the start of a scaling gesture */
393    private int mCellHeightBeforeScaleGesture;
394    /** The hour at the center two touch points */
395    private float mGestureCenterHour = 0;
396    /**
397     * Flag to decide whether to handle the up event. Cases where up events
398     * should be ignored are 1) right after a scale gesture and 2) finger was
399     * down before app launch
400     */
401    private boolean mHandleActionUp = true;
402
403    private int mHoursTextHeight;
404    /**
405     * The height of the area used for allday events
406     */
407    private int mAlldayHeight;
408    /**
409     * The height of the allday event area used during animation
410     */
411    private int mAnimateDayHeight = 0;
412    /**
413     * The height of an individual allday event during animation
414     */
415    private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
416    /**
417     * Whether to use the expand or collapse icon.
418     */
419    private static boolean mUseExpandIcon = true;
420    /**
421     * The height of the day names/numbers
422     */
423    private static int DAY_HEADER_HEIGHT = 45;
424    /**
425     * The height of the day names/numbers for multi-day views
426     */
427    private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT;
428    /**
429     * The height of the day names/numbers when viewing a single day
430     */
431    private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT;
432    /**
433     * Max of all day events in a given day in this view.
434     */
435    private int mMaxAlldayEvents;
436    /**
437     * A count of the number of allday events that were not drawn for each day
438     */
439    private int[] mSkippedAlldayEvents;
440    /**
441     * The number of allDay events at which point we start hiding allDay events.
442     */
443    private int mMaxUnexpandedAlldayEventCount = 4;
444    /**
445     * Whether or not to expand the allDay area to fill the screen
446     */
447    private static boolean mShowAllAllDayEvents = false;
448
449    protected int mNumDays = 7;
450    private int mNumHours = 10;
451
452    /** Width of the time line (list of hours) to the left. */
453    private int mHoursWidth;
454    private int mDateStrWidth;
455    /** Top of the scrollable region i.e. below date labels and all day events */
456    private int mFirstCell;
457    /** First fully visibile hour */
458    private int mFirstHour = -1;
459    /** Distance between the mFirstCell and the top of first fully visible hour. */
460    private int mFirstHourOffset;
461    private String[] mHourStrs;
462    private String[] mDayStrs;
463    private String[] mDayStrs2Letter;
464    private boolean mIs24HourFormat;
465
466    private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
467    private boolean mComputeSelectedEvents;
468    private boolean mUpdateToast;
469    private Event mSelectedEvent;
470    private Event mPrevSelectedEvent;
471    private Rect mPrevBox = new Rect();
472    protected final Resources mResources;
473    protected final Drawable mCurrentTimeLine;
474    protected final Drawable mTodayHeaderDrawable;
475    protected final Drawable mExpandAlldayDrawable;
476    protected final Drawable mCollapseAlldayDrawable;
477    protected Drawable mAcceptedOrTentativeEventBoxDrawable;
478    protected BitmapDrawable mDeclinedBgDrawable;
479    private String mAmString;
480    private String mPmString;
481    private DeleteEventHelper mDeleteEventHelper;
482    private static int sCounter = 0;
483
484    private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
485
486    ScaleGestureDetector mScaleGestureDetector;
487
488    /**
489     * The initial state of the touch mode when we enter this view.
490     */
491    private static final int TOUCH_MODE_INITIAL_STATE = 0;
492
493    /**
494     * Indicates we just received the touch event and we are waiting to see if
495     * it is a tap or a scroll gesture.
496     */
497    private static final int TOUCH_MODE_DOWN = 1;
498
499    /**
500     * Indicates the touch gesture is a vertical scroll
501     */
502    private static final int TOUCH_MODE_VSCROLL = 0x20;
503
504    /**
505     * Indicates the touch gesture is a horizontal scroll
506     */
507    private static final int TOUCH_MODE_HSCROLL = 0x40;
508
509    private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
510
511    /**
512     * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
513     */
514    private static final int SELECTION_HIDDEN = 0;
515    private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet
516    private static final int SELECTION_SELECTED = 2;
517    private static final int SELECTION_LONGPRESS = 3;
518
519    private int mSelectionMode = SELECTION_HIDDEN;
520
521    private boolean mScrolling = false;
522
523    private float mInitialScrollX;
524    private float mInitialScrollY;
525
526    // Animates the height of the allday region
527    ObjectAnimator mAlldayAnimator;
528    // Animates the height of events in the allday region
529    ObjectAnimator mAlldayEventAnimator;
530    // Animates the transparency of the more events text
531    ObjectAnimator mMoreAlldayEventsAnimator;
532    // whether or not an event is stopping because it was cancelled
533    private boolean mCancellingAnimations = false;
534    // tracks whether a touch originated in the allday area
535    private boolean mTouchStartedInAlldayArea = false;
536
537    private CalendarController mController;
538    private ViewSwitcher mViewSwitcher;
539    private GestureDetector mGestureDetector;
540    private OverScroller mScroller;
541    private ScrollInterpolator mHScrollInterpolator;
542
543    public DayView(Context context, CalendarController controller,
544            ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) {
545        super(context);
546        mResources = context.getResources();
547
548        DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size);
549        DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size);
550        ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height);
551        DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin);
552        EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin);
553        HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size);
554        AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size);
555        ALLDAY_TEXT_SIZE = (int) mResources.getDimension(R.dimen.allday_text_size);
556        MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width);
557        HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin);
558        HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin);
559        MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height);
560
561        if (mScale == 0) {
562
563            mScale = mResources.getDisplayMetrics().density;
564            if (mScale != 1) {
565                SINGLE_ALLDAY_HEIGHT *= mScale;
566                ALLDAY_TOP_MARGIN *= mScale;
567                MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale;
568
569                NORMAL_FONT_SIZE *= mScale;
570                EVENT_TEXT_FONT_SIZE *= mScale;
571                GRID_LINE_LEFT_MARGIN *= mScale;
572                HOURS_TOP_MARGIN *= mScale;
573                MIN_CELL_WIDTH_FOR_TEXT *= mScale;
574                MIN_EVENT_HEIGHT *= mScale;
575                MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT *= mScale;
576                MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale;
577                mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
578
579                CURRENT_TIME_LINE_HEIGHT *= mScale;
580                CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
581
582                MIN_Y_SPAN *= mScale;
583                MAX_CELL_HEIGHT *= mScale;
584                DEFAULT_CELL_HEIGHT *= mScale;
585                DAY_HEADER_HEIGHT *= mScale;
586                DAY_HEADER_LEFT_MARGIN *= mScale;
587                DAY_HEADER_RIGHT_MARGIN *= mScale;
588                DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale;
589                DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale;
590                DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale;
591                CALENDAR_COLOR_SQUARE_SIZE *= mScale;
592                EVENT_TEXT_TOP_MARGIN *= mScale;
593                EVENT_TEXT_BOTTOM_MARGIN *= mScale;
594                EVENT_TEXT_LEFT_MARGIN *= mScale;
595                EVENT_TEXT_RIGHT_MARGIN *= mScale;
596                EVENT_ALL_DAY_TEXT_TOP_MARGIN *= mScale;
597                EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN *= mScale;
598                EVENT_ALL_DAY_TEXT_LEFT_MARGIN *= mScale;
599                EVENT_ALL_DAY_TEXT_RIGHT_MARGIN *= mScale;
600                EVENT_RECT_TOP_MARGIN *= mScale;
601                EVENT_RECT_BOTTOM_MARGIN *= mScale;
602                ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale;
603                EVENT_RECT_LEFT_MARGIN *= mScale;
604                EVENT_RECT_RIGHT_MARGIN *= mScale;
605                EVENT_RECT_STROKE_WIDTH *= mScale;
606                EXPAND_ALL_DAY_BOTTOM_MARGIN *= mScale;
607                EVENT_SQUARE_WIDTH *= mScale;
608                EVENT_LINE_PADDING *= mScale;
609            }
610        }
611        HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
612        DAY_HEADER_HEIGHT = numDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT;
613
614        mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_week_holo_light);
615        mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light);
616        mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_expand_holo_light);
617        mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_collapse_holo_light);
618        mAcceptedOrTentativeEventBoxDrawable = mResources
619                .getDrawable(R.drawable.panel_month_event_holo_light);
620
621        mDeclinedBgDrawable = new BitmapDrawable(mResources, BitmapFactory.decodeResource(
622                mResources, R.drawable.event_bg_declined));
623        mDeclinedBgDrawable.setTileModeXY(TileMode.REPEAT, TileMode.REPEAT);
624
625        mEventLoader = eventLoader;
626        mEventGeometry = new EventGeometry();
627        mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
628        mEventGeometry.setHourGap(HOUR_GAP);
629        mEventGeometry.setCellMargin(DAY_GAP);
630        mContext = context;
631        mAllDayString = mContext.getString(R.string.edit_event_all_day_label);
632        mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */);
633        mLastPopupEventID = INVALID_EVENT_ID;
634        mController = controller;
635        mViewSwitcher = viewSwitcher;
636        mGestureDetector = new GestureDetector(context, new CalendarGestureListener());
637        mScaleGestureDetector = new ScaleGestureDetector(getContext(), this);
638        mNumDays = numDays;
639        if (mCellHeight == 0) {
640            mCellHeight = Utils.getSharedPreference(mContext,
641                    GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT);
642        }
643        mScroller = new OverScroller(context);
644        mHScrollInterpolator = new ScrollInterpolator();
645        init(context);
646    }
647
648    private void init(Context context) {
649        setFocusable(true);
650
651        // Allow focus in touch mode so that we can do keyboard shortcuts
652        // even after we've entered touch mode.
653        setFocusableInTouchMode(true);
654        setClickable(true);
655        setOnCreateContextMenuListener(this);
656
657        mFirstDayOfWeek = Utils.getFirstDayOfWeek(context);
658
659        mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater));
660        long currentTime = System.currentTimeMillis();
661        mCurrentTime.set(currentTime);
662        //The % makes it go off at the next increment of 5 minutes.
663        postDelayed(mUpdateCurrentTime,
664                UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
665        mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
666
667        mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
668        mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
669        mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
670        mTodayBgColor = mResources.getColor(R.color.calendar_today_bg_color);
671        mBgColor = mResources.getColor(R.color.calendar_hour_background);
672        mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
673        mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
674        mCalendarGridLineInnerHorizontalColor = mResources
675                .getColor(R.color.calendar_grid_line_inner_horizontal_color);
676        mCalendarGridLineInnerVerticalColor = mResources
677                .getColor(R.color.calendar_grid_line_inner_vertical_color);
678        mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label);
679        mPressedColor = mResources.getColor(R.color.pressed);
680        mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
681        mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color);
682
683        mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
684        mEventTextPaint.setTextAlign(Paint.Align.LEFT);
685        mEventTextPaint.setAntiAlias(true);
686
687        int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
688        Paint p = mSelectionPaint;
689        p.setColor(gridLineColor);
690        p.setStyle(Style.FILL);
691        p.setAntiAlias(false);
692
693        p = mPaint;
694        p.setAntiAlias(true);
695
696        // Allocate space for 2 weeks worth of weekday names so that we can
697        // easily start the week display at any week day.
698        mDayStrs = new String[14];
699
700        // Also create an array of 2-letter abbreviations.
701        mDayStrs2Letter = new String[14];
702
703        for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
704            int index = i - Calendar.SUNDAY;
705            // e.g. Tue for Tuesday
706            mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM)
707                    .toUpperCase();
708            mDayStrs[index + 7] = mDayStrs[index];
709            // e.g. Tu for Tuesday
710            mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT)
711                    .toUpperCase();
712
713            // If we don't have 2-letter day strings, fall back to 1-letter.
714            if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
715                mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
716            }
717
718            mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
719        }
720
721        // Figure out how much space we need for the 3-letter abbrev names
722        // in the worst case.
723        p.setTextSize(DATE_HEADER_FONT_SIZE);
724        p.setTypeface(mBold);
725        String[] dateStrs = {" 28", " 30"};
726        mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
727        p.setTextSize(DAY_HEADER_FONT_SIZE);
728        mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
729
730        p.setTextSize(HOURS_TEXT_SIZE);
731        p.setTypeface(null);
732        handleOnResume();
733
734        mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase();
735        mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase();
736        String[] ampm = {mAmString, mPmString};
737        p.setTextSize(AMPM_TEXT_SIZE);
738        mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p)
739                + HOURS_RIGHT_MARGIN);
740        mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth);
741
742        LayoutInflater inflater;
743        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
744        mPopupView = inflater.inflate(R.layout.bubble_event, null);
745        mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
746                ViewGroup.LayoutParams.MATCH_PARENT,
747                ViewGroup.LayoutParams.WRAP_CONTENT));
748        mPopup = new PopupWindow(context);
749        mPopup.setContentView(mPopupView);
750        Resources.Theme dialogTheme = getResources().newTheme();
751        dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
752        TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
753            android.R.attr.windowBackground });
754        mPopup.setBackgroundDrawable(ta.getDrawable(0));
755        ta.recycle();
756
757        // Enable touching the popup window
758        mPopupView.setOnClickListener(this);
759        // Catch long clicks for creating a new event
760        setOnLongClickListener(this);
761
762        mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater));
763        long millis = System.currentTimeMillis();
764        mBaseDate.set(millis);
765
766        mEarliestStartHour = new int[mNumDays];
767        mHasAllDayEvent = new boolean[mNumDays];
768
769        // mLines is the array of points used with Canvas.drawLines() in
770        // drawGridBackground() and drawAllDayEvents().  Its size depends
771        // on the max number of lines that can ever be drawn by any single
772        // drawLines() call in either of those methods.
773        final int maxGridLines = (24 + 1)  // max horizontal lines we might draw
774                + (mNumDays + 1); // max vertical lines we might draw
775        mLines = new float[maxGridLines * 4];
776    }
777
778    /**
779     * This is called when the popup window is pressed.
780     */
781    public void onClick(View v) {
782        if (v == mPopupView) {
783            // Pretend it was a trackball click because that will always
784            // jump to the "View event" screen.
785            switchViews(true /* trackball */);
786        }
787    }
788
789    public void handleOnResume() {
790        mIs24HourFormat = DateFormat.is24HourFormat(mContext);
791        mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
792        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
793    }
794
795    /**
796     * Returns the start of the selected time in milliseconds since the epoch.
797     *
798     * @return selected time in UTC milliseconds since the epoch.
799     */
800    long getSelectedTimeInMillis() {
801        Time time = new Time(mBaseDate);
802        time.setJulianDay(mSelectionDay);
803        time.hour = mSelectionHour;
804
805        // We ignore the "isDst" field because we want normalize() to figure
806        // out the correct DST value and not adjust the selected time based
807        // on the current setting of DST.
808        return time.normalize(true /* ignore isDst */);
809    }
810
811    Time getSelectedTime() {
812        Time time = new Time(mBaseDate);
813        time.setJulianDay(mSelectionDay);
814        time.hour = mSelectionHour;
815
816        // We ignore the "isDst" field because we want normalize() to figure
817        // out the correct DST value and not adjust the selected time based
818        // on the current setting of DST.
819        time.normalize(true /* ignore isDst */);
820        return time;
821    }
822
823    /**
824     * Returns the start of the selected time in minutes since midnight,
825     * local time.  The derived class must ensure that this is consistent
826     * with the return value from getSelectedTimeInMillis().
827     */
828    int getSelectedMinutesSinceMidnight() {
829        return mSelectionHour * MINUTES_PER_HOUR;
830    }
831
832    int getFirstVisibleHour() {
833        return mFirstHour;
834    }
835
836    void setFirstVisibleHour(int firstHour) {
837        mFirstHour = firstHour;
838        mFirstHourOffset = 0;
839    }
840
841    public void setSelected(Time time, boolean ignoreTime) {
842        mBaseDate.set(time);
843        mSelectionHour = mBaseDate.hour;
844        mSelectedEvent = null;
845        mPrevSelectedEvent = null;
846        long millis = mBaseDate.toMillis(false /* use isDst */);
847        mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
848        mSelectedEvents.clear();
849        mComputeSelectedEvents = true;
850
851        int gotoY = Integer.MIN_VALUE;
852
853        if (!ignoreTime && mGridAreaHeight != -1) {
854            int lastHour = 0;
855
856            if (mBaseDate.hour < mFirstHour) {
857                // Above visible region
858                gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP);
859            } else {
860                lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP)
861                        + mFirstHour;
862
863                if (mBaseDate.hour >= lastHour) {
864                    // Below visible region
865
866                    // target hour + 1 (to give it room to see the event) -
867                    // grid height (to get the y of the top of the visible
868                    // region)
869                    gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f)
870                            * (mCellHeight + HOUR_GAP) - mGridAreaHeight);
871                }
872            }
873
874            if (DEBUG) {
875                Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH "
876                        + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight
877                        + " ymax " + mMaxViewStartY);
878            }
879
880            if (gotoY > mMaxViewStartY) {
881                gotoY = mMaxViewStartY;
882            } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) {
883                gotoY = 0;
884            }
885        }
886
887        recalc();
888
889        // Don't draw the selection box if we are going to the "current" time
890        long currMillis = System.currentTimeMillis();
891        boolean recent = (currMillis - 10000) < millis && millis < currMillis;
892        mSelectionMode = (recent || ignoreTime) ? SELECTION_HIDDEN : SELECTION_SELECTED;
893        mRemeasure = true;
894        invalidate();
895
896        if (gotoY != Integer.MIN_VALUE) {
897            TypeEvaluator evaluator = new TypeEvaluator() {
898                @Override
899                public Object evaluate(float fraction, Object startValue, Object endValue) {
900                    int start = (Integer) startValue;
901                    int end = (Integer) endValue;
902                    final int newValue = (int) ((end - start) * fraction + start);
903                    setViewStartY(newValue);
904                    return new Integer(newValue);
905                }
906            };
907            ValueAnimator scrollAnim = ObjectAnimator.ofObject(evaluator, new Integer(mViewStartY),
908                    new Integer(gotoY));
909//          TODO The following line is supposed to replace the two statements above.
910//          Need to investigate why it's not working.
911
912//          ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY);
913            scrollAnim.setDuration(200);
914            scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator());
915            scrollAnim.addListener(mAnimatorListener);
916            scrollAnim.start();
917        }
918    }
919
920    public void setViewStartY(int viewStartY) {
921        if (viewStartY > mMaxViewStartY) {
922            viewStartY = mMaxViewStartY;
923        }
924
925        mViewStartY = viewStartY;
926
927        computeFirstHour();
928        invalidate();
929    }
930
931    public Time getSelectedDay() {
932        Time time = new Time(mBaseDate);
933        time.setJulianDay(mSelectionDay);
934        time.hour = mSelectionHour;
935
936        // We ignore the "isDst" field because we want normalize() to figure
937        // out the correct DST value and not adjust the selected time based
938        // on the current setting of DST.
939        time.normalize(true /* ignore isDst */);
940        return time;
941    }
942
943    public void updateTitle() {
944        Time start = new Time(mBaseDate);
945        start.normalize(true);
946        Time end = new Time(start);
947        end.monthDay += mNumDays - 1;
948        // Move it forward one minute so the formatter doesn't lose a day
949        end.minute += 1;
950        end.normalize(true);
951
952        long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
953        if (mNumDays != 1) {
954            // Don't show day of the month if for multi-day view
955            formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY;
956
957            // Abbreviate the month if showing multiple months
958            if (start.month != end.month) {
959                formatFlags |= DateUtils.FORMAT_ABBREV_MONTH;
960            }
961        }
962
963        mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT,
964                formatFlags, null, null);
965    }
966
967    /**
968     * return a negative number if "time" is comes before the visible time
969     * range, a positive number if "time" is after the visible time range, and 0
970     * if it is in the visible time range.
971     */
972    public int compareToVisibleTimeRange(Time time) {
973
974        int savedHour = mBaseDate.hour;
975        int savedMinute = mBaseDate.minute;
976        int savedSec = mBaseDate.second;
977
978        mBaseDate.hour = 0;
979        mBaseDate.minute = 0;
980        mBaseDate.second = 0;
981
982        if (DEBUG) {
983            Log.d(TAG, "Begin " + mBaseDate.toString());
984            Log.d(TAG, "Diff  " + time.toString());
985        }
986
987        // Compare beginning of range
988        int diff = Time.compare(time, mBaseDate);
989        if (diff > 0) {
990            // Compare end of range
991            mBaseDate.monthDay += mNumDays;
992            mBaseDate.normalize(true);
993            diff = Time.compare(time, mBaseDate);
994
995            if (DEBUG) Log.d(TAG, "End   " + mBaseDate.toString());
996
997            mBaseDate.monthDay -= mNumDays;
998            mBaseDate.normalize(true);
999            if (diff < 0) {
1000                // in visible time
1001                diff = 0;
1002            } else if (diff == 0) {
1003                // Midnight of following day
1004                diff = 1;
1005            }
1006        }
1007
1008        if (DEBUG) Log.d(TAG, "Diff: " + diff);
1009
1010        mBaseDate.hour = savedHour;
1011        mBaseDate.minute = savedMinute;
1012        mBaseDate.second = savedSec;
1013        return diff;
1014    }
1015
1016    private void recalc() {
1017        // Set the base date to the beginning of the week if we are displaying
1018        // 7 days at a time.
1019        if (mNumDays == 7) {
1020            adjustToBeginningOfWeek(mBaseDate);
1021        }
1022
1023        final long start = mBaseDate.toMillis(false /* use isDst */);
1024        mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
1025        mLastJulianDay = mFirstJulianDay + mNumDays - 1;
1026
1027        mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
1028        mFirstVisibleDate = mBaseDate.monthDay;
1029        mFirstVisibleDayOfWeek = mBaseDate.weekDay;
1030    }
1031
1032    private void adjustToBeginningOfWeek(Time time) {
1033        int dayOfWeek = time.weekDay;
1034        int diff = dayOfWeek - mFirstDayOfWeek;
1035        if (diff != 0) {
1036            if (diff < 0) {
1037                diff += 7;
1038            }
1039            time.monthDay -= diff;
1040            time.normalize(true /* ignore isDst */);
1041        }
1042    }
1043
1044    @Override
1045    protected void onSizeChanged(int width, int height, int oldw, int oldh) {
1046        mViewWidth = width;
1047        mViewHeight = height;
1048        int gridAreaWidth = width - mHoursWidth;
1049        mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
1050
1051        // This would be about 1 day worth in a 7 day view
1052        mHorizontalSnapBackThreshold = width / 7;
1053
1054        Paint p = new Paint();
1055        p.setTextSize(HOURS_TEXT_SIZE);
1056        mHoursTextHeight = (int) Math.abs(p.ascent());
1057        remeasure(width, height);
1058    }
1059
1060    /**
1061     * Measures the space needed for various parts of the view after
1062     * loading new events.  This can change if there are all-day events.
1063     */
1064    private void remeasure(int width, int height) {
1065        // Shrink to fit available space but make sure we can display at least two events
1066        MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4);
1067        MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6);
1068        MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT,
1069                (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2);
1070        mMaxUnexpandedAlldayEventCount =
1071                (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1072
1073        // First, clear the array of earliest start times, and the array
1074        // indicating presence of an all-day event.
1075        for (int day = 0; day < mNumDays; day++) {
1076            mEarliestStartHour[day] = 25;  // some big number
1077            mHasAllDayEvent[day] = false;
1078        }
1079
1080        int maxAllDayEvents = mMaxAlldayEvents;
1081
1082        // The min is where 24 hours cover the entire visible area
1083        mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT);
1084        if (mCellHeight < mMinCellHeight) {
1085            mCellHeight = mMinCellHeight;
1086        }
1087
1088        // Calculate mAllDayHeight
1089        mFirstCell = DAY_HEADER_HEIGHT;
1090        int allDayHeight = 0;
1091        if (maxAllDayEvents > 0) {
1092            int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT;
1093            // If there is at most one all-day event per day, then use less
1094            // space (but more than the space for a single event).
1095            if (maxAllDayEvents == 1) {
1096                allDayHeight = SINGLE_ALLDAY_HEIGHT;
1097            } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){
1098                // Allow the all-day area to grow in height depending on the
1099                // number of all-day events we need to show, up to a limit.
1100                allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
1101                if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
1102                    allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT;
1103                }
1104            } else {
1105                // if we have more than the magic number, check if we're animating
1106                // and if not adjust the sizes appropriately
1107                if (mAnimateDayHeight != 0) {
1108                    // Don't shrink the space past the final allDay space. The animation
1109                    // continues to hide the last event so the more events text can
1110                    // fade in.
1111                    allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT);
1112                } else {
1113                    // Try to fit all the events in
1114                    allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1115                    // But clip the area depending on which mode we're in
1116                    if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
1117                        allDayHeight = (int) (mMaxUnexpandedAlldayEventCount *
1118                                MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT);
1119                    } else if (allDayHeight > maxAllAllDayHeight) {
1120                        allDayHeight = maxAllAllDayHeight;
1121                    }
1122                }
1123            }
1124            mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN;
1125        } else {
1126            mSelectionAllday = false;
1127        }
1128        mAlldayHeight = allDayHeight;
1129
1130        mGridAreaHeight = height - mFirstCell;
1131
1132        // Set up the expand icon position
1133        int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth();
1134        mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2,
1135                EVENT_ALL_DAY_TEXT_LEFT_MARGIN);
1136        mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth
1137                - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN);
1138        mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN;
1139        mExpandAllDayRect.top = mExpandAllDayRect.bottom
1140                - mExpandAlldayDrawable.getIntrinsicHeight();
1141
1142        // Cause the allDay text to be relaid out on the next draw pass
1143        mAllDayTextLayout = null;
1144
1145
1146        mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP);
1147        mEventGeometry.setHourHeight(mCellHeight);
1148
1149        final long minimumDurationMillis = (long)
1150                (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f));
1151        Event.computePositions(mEvents, minimumDurationMillis);
1152
1153        // Compute the top of our reachable view
1154        mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight;
1155        if (DEBUG) {
1156            Log.e(TAG, "mViewStartY: " + mViewStartY);
1157            Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY);
1158        }
1159        if (mViewStartY > mMaxViewStartY) {
1160            mViewStartY = mMaxViewStartY;
1161            computeFirstHour();
1162        }
1163
1164        if (mFirstHour == -1) {
1165            initFirstHour();
1166            mFirstHourOffset = 0;
1167        }
1168
1169        // When we change the base date, the number of all-day events may
1170        // change and that changes the cell height.  When we switch dates,
1171        // we use the mFirstHourOffset from the previous view, but that may
1172        // be too large for the new view if the cell height is smaller.
1173        if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
1174            mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
1175        }
1176        mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
1177
1178        final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
1179        //When we get new events we don't want to dismiss the popup unless the event changes
1180        if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
1181            mPopup.dismiss();
1182        }
1183        mPopup.setWidth(eventAreaWidth - 20);
1184        mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
1185    }
1186
1187    /**
1188     * Initialize the state for another view.  The given view is one that has
1189     * its own bitmap and will use an animation to replace the current view.
1190     * The current view and new view are either both Week views or both Day
1191     * views.  They differ in their base date.
1192     *
1193     * @param view the view to initialize.
1194     */
1195    private void initView(DayView view) {
1196        view.mSelectionHour = mSelectionHour;
1197        view.mSelectedEvents.clear();
1198        view.mComputeSelectedEvents = true;
1199        view.mFirstHour = mFirstHour;
1200        view.mFirstHourOffset = mFirstHourOffset;
1201        view.remeasure(getWidth(), getHeight());
1202        view.initAllDayHeights();
1203
1204        view.mSelectedEvent = null;
1205        view.mPrevSelectedEvent = null;
1206        view.mFirstDayOfWeek = mFirstDayOfWeek;
1207        if (view.mEvents.size() > 0) {
1208            view.mSelectionAllday = mSelectionAllday;
1209        } else {
1210            view.mSelectionAllday = false;
1211        }
1212
1213        // Redraw the screen so that the selection box will be redrawn.  We may
1214        // have scrolled to a different part of the day in some other view
1215        // so the selection box in this view may no longer be visible.
1216        view.recalc();
1217    }
1218
1219    /**
1220     * Switch to another view based on what was selected (an event or a free
1221     * slot) and how it was selected (by touch or by trackball).
1222     *
1223     * @param trackBallSelection true if the selection was made using the
1224     * trackball.
1225     */
1226    private void switchViews(boolean trackBallSelection) {
1227        Event selectedEvent = mSelectedEvent;
1228
1229        mPopup.dismiss();
1230        mLastPopupEventID = INVALID_EVENT_ID;
1231        if (mNumDays > 1) {
1232            // This is the Week view.
1233            // With touch, we always switch to Day/Agenda View
1234            // With track ball, if we selected a free slot, then create an event.
1235            // If we selected a specific event, switch to EventInfo view.
1236            if (trackBallSelection) {
1237                if (selectedEvent == null) {
1238                    // Switch to the EditEvent view
1239                    long startMillis = getSelectedTimeInMillis();
1240                    long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
1241                    mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1,
1242                            startMillis, endMillis, 0, 0, -1);
1243                } else {
1244                    // Switch to the EventInfo view
1245                    mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1246                            selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1247                            getSelectedTimeInMillis());
1248                }
1249            } else {
1250                // This was a touch selection.  If the touch selected a single
1251                // unambiguous event, then view that event.  Otherwise go to
1252                // Day/Agenda view.
1253                if (mSelectedEvents.size() == 1) {
1254                    mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1255                            selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1256                            getSelectedTimeInMillis());
1257                }
1258            }
1259        } else {
1260            // This is the Day view.
1261            // If we selected a free slot, then create an event.
1262            // If we selected an event, then go to the EventInfo view.
1263            if (selectedEvent == null) {
1264                // Switch to the EditEvent view
1265                long startMillis = getSelectedTimeInMillis();
1266                long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
1267
1268                mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, startMillis,
1269                        endMillis, 0, 0, -1);
1270            } else {
1271                mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id,
1272                        selectedEvent.startMillis, selectedEvent.endMillis, 0, 0,
1273                        getSelectedTimeInMillis());
1274            }
1275        }
1276    }
1277
1278    @Override
1279    public boolean onKeyUp(int keyCode, KeyEvent event) {
1280        mScrolling = false;
1281        long duration = event.getEventTime() - event.getDownTime();
1282
1283        switch (keyCode) {
1284            case KeyEvent.KEYCODE_DPAD_CENTER:
1285                if (mSelectionMode == SELECTION_HIDDEN) {
1286                    // Don't do anything unless the selection is visible.
1287                    break;
1288                }
1289
1290                if (mSelectionMode == SELECTION_PRESSED) {
1291                    // This was the first press when there was nothing selected.
1292                    // Change the selection from the "pressed" state to the
1293                    // the "selected" state.  We treat short-press and
1294                    // long-press the same here because nothing was selected.
1295                    mSelectionMode = SELECTION_SELECTED;
1296                    invalidate();
1297                    break;
1298                }
1299
1300                // Check the duration to determine if this was a short press
1301                if (duration < ViewConfiguration.getLongPressTimeout()) {
1302                    switchViews(true /* trackball */);
1303                } else {
1304                    mSelectionMode = SELECTION_LONGPRESS;
1305                    invalidate();
1306                    performLongClick();
1307                }
1308                break;
1309//            case KeyEvent.KEYCODE_BACK:
1310//                if (event.isTracking() && !event.isCanceled()) {
1311//                    mPopup.dismiss();
1312//                    mContext.finish();
1313//                    return true;
1314//                }
1315//                break;
1316        }
1317        return super.onKeyUp(keyCode, event);
1318    }
1319
1320    @Override
1321    public boolean onKeyDown(int keyCode, KeyEvent event) {
1322        if (mSelectionMode == SELECTION_HIDDEN) {
1323            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1324                    || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
1325                    || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
1326                // Display the selection box but don't move or select it
1327                // on this key press.
1328                mSelectionMode = SELECTION_SELECTED;
1329                invalidate();
1330                return true;
1331            } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
1332                // Display the selection box but don't select it
1333                // on this key press.
1334                mSelectionMode = SELECTION_PRESSED;
1335                invalidate();
1336                return true;
1337            }
1338        }
1339
1340        mSelectionMode = SELECTION_SELECTED;
1341        mScrolling = false;
1342        boolean redraw;
1343        int selectionDay = mSelectionDay;
1344
1345        switch (keyCode) {
1346            case KeyEvent.KEYCODE_DEL:
1347                // Delete the selected event, if any
1348                Event selectedEvent = mSelectedEvent;
1349                if (selectedEvent == null) {
1350                    return false;
1351                }
1352                mPopup.dismiss();
1353                mLastPopupEventID = INVALID_EVENT_ID;
1354
1355                long begin = selectedEvent.startMillis;
1356                long end = selectedEvent.endMillis;
1357                long id = selectedEvent.id;
1358                mDeleteEventHelper.delete(begin, end, id, -1);
1359                return true;
1360            case KeyEvent.KEYCODE_ENTER:
1361                switchViews(true /* trackball or keyboard */);
1362                return true;
1363            case KeyEvent.KEYCODE_BACK:
1364                if (event.getRepeatCount() == 0) {
1365                    event.startTracking();
1366                    return true;
1367                }
1368                return super.onKeyDown(keyCode, event);
1369            case KeyEvent.KEYCODE_DPAD_LEFT:
1370                if (mSelectedEvent != null) {
1371                    mSelectedEvent = mSelectedEvent.nextLeft;
1372                }
1373                if (mSelectedEvent == null) {
1374                    mLastPopupEventID = INVALID_EVENT_ID;
1375                    selectionDay -= 1;
1376                }
1377                redraw = true;
1378                break;
1379
1380            case KeyEvent.KEYCODE_DPAD_RIGHT:
1381                if (mSelectedEvent != null) {
1382                    mSelectedEvent = mSelectedEvent.nextRight;
1383                }
1384                if (mSelectedEvent == null) {
1385                    mLastPopupEventID = INVALID_EVENT_ID;
1386                    selectionDay += 1;
1387                }
1388                redraw = true;
1389                break;
1390
1391            case KeyEvent.KEYCODE_DPAD_UP:
1392                if (mSelectedEvent != null) {
1393                    mSelectedEvent = mSelectedEvent.nextUp;
1394                }
1395                if (mSelectedEvent == null) {
1396                    mLastPopupEventID = INVALID_EVENT_ID;
1397                    if (!mSelectionAllday) {
1398                        mSelectionHour -= 1;
1399                        adjustHourSelection();
1400                        mSelectedEvents.clear();
1401                        mComputeSelectedEvents = true;
1402                    }
1403                }
1404                redraw = true;
1405                break;
1406
1407            case KeyEvent.KEYCODE_DPAD_DOWN:
1408                if (mSelectedEvent != null) {
1409                    mSelectedEvent = mSelectedEvent.nextDown;
1410                }
1411                if (mSelectedEvent == null) {
1412                    mLastPopupEventID = INVALID_EVENT_ID;
1413                    if (mSelectionAllday) {
1414                        mSelectionAllday = false;
1415                    } else {
1416                        mSelectionHour++;
1417                        adjustHourSelection();
1418                        mSelectedEvents.clear();
1419                        mComputeSelectedEvents = true;
1420                    }
1421                }
1422                redraw = true;
1423                break;
1424
1425            default:
1426                return super.onKeyDown(keyCode, event);
1427        }
1428
1429        if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1430            DayView view = (DayView) mViewSwitcher.getNextView();
1431            Time date = view.mBaseDate;
1432            date.set(mBaseDate);
1433            if (selectionDay < mFirstJulianDay) {
1434                date.monthDay -= mNumDays;
1435            } else {
1436                date.monthDay += mNumDays;
1437            }
1438            date.normalize(true /* ignore isDst */);
1439            view.mSelectionDay = selectionDay;
1440
1441            initView(view);
1442
1443            Time end = new Time(date);
1444            end.monthDay += mNumDays - 1;
1445            mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT);
1446            return true;
1447        }
1448        if (mSelectionDay != selectionDay) {
1449            Time date = new Time(mBaseDate);
1450            date.setJulianDay(selectionDay);
1451            date.hour = mSelectionHour;
1452            mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT);
1453        }
1454        mSelectionDay = selectionDay;
1455        mSelectedEvents.clear();
1456        mComputeSelectedEvents = true;
1457        mUpdateToast = true;
1458
1459        if (redraw) {
1460            invalidate();
1461            return true;
1462        }
1463
1464        return super.onKeyDown(keyCode, event);
1465    }
1466
1467    private class AccessibilityRunnable implements Runnable {
1468        int mEventType = AccessibilityEvent.TYPE_VIEW_SELECTED;
1469        @Override
1470        public void run() {
1471            sendAccessibilityEvent(mEventType);
1472        }
1473    }
1474
1475    private AccessibilityRunnable mDispatchAccessibilityEventRunnable = new AccessibilityRunnable();
1476
1477    @Override
1478    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1479        if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SELECTED &&
1480                event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
1481            return false;
1482        }
1483        StringBuilder b = new StringBuilder(getSelectedTime()
1484                .format(mIs24HourFormat ? HOUR_FORMAT_24H : HOUR_FORMAT_12H));
1485        b.append(PERIOD_SPACE);
1486        int numEvents = mSelectedEvents.size();
1487        if (mEventCountTemplate == null) {
1488            mEventCountTemplate = mContext.getString(R.string.template_announce_item_index);
1489        }
1490        switch (event.getEventType()) {
1491            // When a new hour is selected we sent this event
1492            case AccessibilityEvent.TYPE_VIEW_SELECTED:
1493                int i = 1;
1494                for (Event calEvent : mSelectedEvents) {
1495                    if (numEvents > 1) {
1496                        mStringBuilder.setLength(0);
1497                        b.append(mFormatter.format(mEventCountTemplate, i++, numEvents));
1498                        b.append(" ");
1499                    }
1500                    appendEventAccessibilityString(b, calEvent);
1501                }
1502                break;
1503            // When a different event is selected we send this event
1504            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
1505                if (mSelectedEvent != null) {
1506                    if (numEvents > 1) {
1507                        mStringBuilder.setLength(0);
1508                        b.append(mFormatter.format(mEventCountTemplate,
1509                                mSelectedEvents.indexOf(mSelectedEvent) + 1, numEvents));
1510                        b.append(" ");
1511                    }
1512                    appendEventAccessibilityString(b, mSelectedEvent);
1513                }
1514                break;
1515            default:
1516                break;
1517        }
1518        CharSequence msg = b.toString();
1519        event.getText().add(msg);
1520        event.setAddedCount(msg.length());
1521        return true;
1522    }
1523
1524    /**
1525     * @param b
1526     * @param calEvent
1527     */
1528    private void appendEventAccessibilityString(StringBuilder b, Event calEvent) {
1529        b.append(calEvent.getTitleAndLocation());
1530        b.append(PERIOD_SPACE);
1531        String when;
1532        int flags = DateUtils.FORMAT_SHOW_DATE;
1533        if (calEvent.allDay) {
1534            flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY;
1535        } else {
1536            flags |= DateUtils.FORMAT_SHOW_TIME;
1537            if (DateFormat.is24HourFormat(mContext)) {
1538                flags |= DateUtils.FORMAT_24HOUR;
1539            }
1540        }
1541        when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags);
1542        b.append(when);
1543        b.append(PERIOD_SPACE);
1544    }
1545
1546    private class GotoBroadcaster implements Animation.AnimationListener {
1547        private final int mCounter;
1548        private final Time mStart;
1549        private final Time mEnd;
1550
1551        public GotoBroadcaster(Time start, Time end) {
1552            mCounter = ++sCounter;
1553            mStart = start;
1554            mEnd = end;
1555        }
1556
1557        @Override
1558        public void onAnimationEnd(Animation animation) {
1559            DayView view = (DayView) mViewSwitcher.getCurrentView();
1560            view.mViewStartX = 0;
1561            view = (DayView) mViewSwitcher.getNextView();
1562            view.mViewStartX = 0;
1563
1564            if (mCounter == sCounter) {
1565                mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1,
1566                        ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null);
1567            }
1568        }
1569
1570        @Override
1571        public void onAnimationRepeat(Animation animation) {
1572        }
1573
1574        @Override
1575        public void onAnimationStart(Animation animation) {
1576        }
1577    }
1578
1579    private View switchViews(boolean forward, float xOffSet, float width, float velocity) {
1580        mAnimationDistance = width - xOffSet;
1581        if (DEBUG) {
1582            Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance);
1583        }
1584
1585        float progress = Math.abs(xOffSet) / width;
1586        if (progress > 1.0f) {
1587            progress = 1.0f;
1588        }
1589
1590        float inFromXValue, inToXValue;
1591        float outFromXValue, outToXValue;
1592        if (forward) {
1593            inFromXValue = 1.0f - progress;
1594            inToXValue = 0.0f;
1595            outFromXValue = -progress;
1596            outToXValue = -1.0f;
1597        } else {
1598            inFromXValue = progress - 1.0f;
1599            inToXValue = 0.0f;
1600            outFromXValue = progress;
1601            outToXValue = 1.0f;
1602        }
1603
1604        final Time start = new Time(mBaseDate.timezone);
1605        start.set(mController.getTime());
1606        if (forward) {
1607            start.monthDay += mNumDays;
1608        } else {
1609            start.monthDay -= mNumDays;
1610        }
1611        mController.setTime(start.normalize(true));
1612
1613        Time newSelected = start;
1614
1615        if (mNumDays == 7) {
1616            newSelected = new Time(start);
1617            adjustToBeginningOfWeek(start);
1618        }
1619
1620        final Time end = new Time(start);
1621        end.monthDay += mNumDays - 1;
1622
1623        // We have to allocate these animation objects each time we switch views
1624        // because that is the only way to set the animation parameters.
1625        TranslateAnimation inAnimation = new TranslateAnimation(
1626                Animation.RELATIVE_TO_SELF, inFromXValue,
1627                Animation.RELATIVE_TO_SELF, inToXValue,
1628                Animation.ABSOLUTE, 0.0f,
1629                Animation.ABSOLUTE, 0.0f);
1630
1631        TranslateAnimation outAnimation = new TranslateAnimation(
1632                Animation.RELATIVE_TO_SELF, outFromXValue,
1633                Animation.RELATIVE_TO_SELF, outToXValue,
1634                Animation.ABSOLUTE, 0.0f,
1635                Animation.ABSOLUTE, 0.0f);
1636
1637        long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity);
1638        inAnimation.setDuration(duration);
1639        inAnimation.setInterpolator(mHScrollInterpolator);
1640        outAnimation.setInterpolator(mHScrollInterpolator);
1641        outAnimation.setDuration(duration);
1642        outAnimation.setAnimationListener(new GotoBroadcaster(start, end));
1643        mViewSwitcher.setInAnimation(inAnimation);
1644        mViewSwitcher.setOutAnimation(outAnimation);
1645
1646        DayView view = (DayView) mViewSwitcher.getCurrentView();
1647        view.cleanup();
1648        mViewSwitcher.showNext();
1649        view = (DayView) mViewSwitcher.getCurrentView();
1650        view.setSelected(newSelected, true);
1651        view.requestFocus();
1652        view.reloadEvents();
1653        view.updateTitle();
1654
1655        return view;
1656    }
1657
1658    // This is called after scrolling stops to move the selected hour
1659    // to the visible part of the screen.
1660    private void resetSelectedHour() {
1661        if (mSelectionHour < mFirstHour + 1) {
1662            mSelectionHour = mFirstHour + 1;
1663            mSelectedEvent = null;
1664            mSelectedEvents.clear();
1665            mComputeSelectedEvents = true;
1666        } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1667            mSelectionHour = mFirstHour + mNumHours - 3;
1668            mSelectedEvent = null;
1669            mSelectedEvents.clear();
1670            mComputeSelectedEvents = true;
1671        }
1672    }
1673
1674    private void initFirstHour() {
1675        mFirstHour = mSelectionHour - mNumHours / 5;
1676        if (mFirstHour < 0) {
1677            mFirstHour = 0;
1678        } else if (mFirstHour + mNumHours > 24) {
1679            mFirstHour = 24 - mNumHours;
1680        }
1681    }
1682
1683    /**
1684     * Recomputes the first full hour that is visible on screen after the
1685     * screen is scrolled.
1686     */
1687    private void computeFirstHour() {
1688        // Compute the first full hour that is visible on screen
1689        mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1690        mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1691    }
1692
1693    private void adjustHourSelection() {
1694        if (mSelectionHour < 0) {
1695            mSelectionHour = 0;
1696            if (mMaxAlldayEvents > 0) {
1697                mPrevSelectedEvent = null;
1698                mSelectionAllday = true;
1699            }
1700        }
1701
1702        if (mSelectionHour > 23) {
1703            mSelectionHour = 23;
1704        }
1705
1706        // If the selected hour is at least 2 time slots from the top and
1707        // bottom of the screen, then don't scroll the view.
1708        if (mSelectionHour < mFirstHour + 1) {
1709            // If there are all-days events for the selected day but there
1710            // are no more normal events earlier in the day, then jump to
1711            // the all-day event area.
1712            // Exception 1: allow the user to scroll to 8am with the trackball
1713            // before jumping to the all-day event area.
1714            // Exception 2: if 12am is on screen, then allow the user to select
1715            // 12am before going up to the all-day event area.
1716            int daynum = mSelectionDay - mFirstJulianDay;
1717            if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1718                    && mFirstHour > 0 && mFirstHour < 8) {
1719                mPrevSelectedEvent = null;
1720                mSelectionAllday = true;
1721                mSelectionHour = mFirstHour + 1;
1722                return;
1723            }
1724
1725            if (mFirstHour > 0) {
1726                mFirstHour -= 1;
1727                mViewStartY -= (mCellHeight + HOUR_GAP);
1728                if (mViewStartY < 0) {
1729                    mViewStartY = 0;
1730                }
1731                return;
1732            }
1733        }
1734
1735        if (mSelectionHour > mFirstHour + mNumHours - 3) {
1736            if (mFirstHour < 24 - mNumHours) {
1737                mFirstHour += 1;
1738                mViewStartY += (mCellHeight + HOUR_GAP);
1739                if (mViewStartY > mMaxViewStartY) {
1740                    mViewStartY = mMaxViewStartY;
1741                }
1742                return;
1743            } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1744                mViewStartY = mMaxViewStartY;
1745            }
1746        }
1747    }
1748
1749    void clearCachedEvents() {
1750        mLastReloadMillis = 0;
1751    }
1752
1753    private Runnable mCancelCallback = new Runnable() {
1754        public void run() {
1755            clearCachedEvents();
1756        }
1757    };
1758
1759    /* package */ void reloadEvents() {
1760        // Protect against this being called before this view has been
1761        // initialized.
1762//        if (mContext == null) {
1763//            return;
1764//        }
1765
1766        // Make sure our time zones are up to date
1767        mTZUpdater.run();
1768
1769        mSelectedEvent = null;
1770        mPrevSelectedEvent = null;
1771        mSelectedEvents.clear();
1772
1773        // The start date is the beginning of the week at 12am
1774        Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater));
1775        weekStart.set(mBaseDate);
1776        weekStart.hour = 0;
1777        weekStart.minute = 0;
1778        weekStart.second = 0;
1779        long millis = weekStart.normalize(true /* ignore isDst */);
1780
1781        // Avoid reloading events unnecessarily.
1782        if (millis == mLastReloadMillis) {
1783            return;
1784        }
1785        mLastReloadMillis = millis;
1786
1787        // load events in the background
1788//        mContext.startProgressSpinner();
1789        final ArrayList<Event> events = new ArrayList<Event>();
1790        mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() {
1791            public void run() {
1792                mEvents = events;
1793                if (mAllDayEvents == null) {
1794                    mAllDayEvents = new ArrayList<Event>();
1795                } else {
1796                    mAllDayEvents.clear();
1797                }
1798
1799                // Create a shorter array for all day events
1800                for (Event e : events) {
1801                    if (e.drawAsAllday()) {
1802                        mAllDayEvents.add(e);
1803                    }
1804                }
1805
1806                // New events, new layouts
1807                if (mLayouts == null || mLayouts.length < events.size()) {
1808                    mLayouts = new StaticLayout[events.size()];
1809                } else {
1810                    Arrays.fill(mLayouts, null);
1811                }
1812
1813                if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) {
1814                    mAllDayLayouts = new StaticLayout[events.size()];
1815                } else {
1816                    Arrays.fill(mAllDayLayouts, null);
1817                }
1818
1819                computeEventRelations();
1820
1821                mRemeasure = true;
1822                mComputeSelectedEvents = true;
1823                recalc();
1824//                mContext.stopProgressSpinner();
1825                invalidate();
1826            }
1827        }, mCancelCallback);
1828    }
1829
1830    private void computeEventRelations() {
1831        // Compute the layout relation between each event before measuring cell
1832        // width, as the cell width should be adjusted along with the relation.
1833        //
1834        // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm)
1835        // We should mark them as "overwapped". Though they are not overwapped logically, but
1836        // minimum cell height implicitly expands the cell height of A and it should look like
1837        // (1:00pm - 1:15pm) after the cell height adjustment.
1838
1839        // Compute the space needed for the all-day events, if any.
1840        // Make a pass over all the events, and keep track of the maximum
1841        // number of all-day events in any one day.  Also, keep track of
1842        // the earliest event in each day.
1843        int maxAllDayEvents = 0;
1844        final ArrayList<Event> events = mEvents;
1845        final int len = events.size();
1846        // Num of all-day-events on each day.
1847        final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1];
1848        Arrays.fill(eventsCount, 0);
1849        for (int ii = 0; ii < len; ii++) {
1850            Event event = events.get(ii);
1851            if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) {
1852                continue;
1853            }
1854            if (event.drawAsAllday()) {
1855                // Count all the events being drawn as allDay events
1856                final int firstDay = Math.max(event.startDay, mFirstJulianDay);
1857                final int lastDay = Math.min(event.endDay, mLastJulianDay);
1858                for (int day = firstDay; day <= lastDay; day++) {
1859                    final int count = ++eventsCount[day - mFirstJulianDay];
1860                    if (maxAllDayEvents < count) {
1861                        maxAllDayEvents = count;
1862                    }
1863                }
1864
1865                int daynum = event.startDay - mFirstJulianDay;
1866                int durationDays = event.endDay - event.startDay + 1;
1867                if (daynum < 0) {
1868                    durationDays += daynum;
1869                    daynum = 0;
1870                }
1871                if (daynum + durationDays > mNumDays) {
1872                    durationDays = mNumDays - daynum;
1873                }
1874                for (int day = daynum; durationDays > 0; day++, durationDays--) {
1875                    mHasAllDayEvent[day] = true;
1876                }
1877            } else {
1878                int daynum = event.startDay - mFirstJulianDay;
1879                int hour = event.startTime / 60;
1880                if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
1881                    mEarliestStartHour[daynum] = hour;
1882                }
1883
1884                // Also check the end hour in case the event spans more than
1885                // one day.
1886                daynum = event.endDay - mFirstJulianDay;
1887                hour = event.endTime / 60;
1888                if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
1889                    mEarliestStartHour[daynum] = hour;
1890                }
1891            }
1892        }
1893        mMaxAlldayEvents = maxAllDayEvents;
1894        initAllDayHeights();
1895    }
1896
1897    @Override
1898    protected void onDraw(Canvas canvas) {
1899        if (mRemeasure) {
1900            remeasure(getWidth(), getHeight());
1901            mRemeasure = false;
1902        }
1903        canvas.save();
1904
1905        float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight;
1906        // offset canvas by the current drag and header position
1907        canvas.translate(-mViewStartX, yTranslate);
1908        // clip to everything below the allDay area
1909        Rect dest = mDestRect;
1910        dest.top = (int) (mFirstCell - yTranslate);
1911        dest.bottom = (int) (mViewHeight - yTranslate);
1912        dest.left = 0;
1913        dest.right = mViewWidth;
1914        canvas.save();
1915        canvas.clipRect(dest);
1916        // Draw the movable part of the view
1917        doDraw(canvas);
1918        // restore to having no clip
1919        canvas.restore();
1920
1921        if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1922            float xTranslate;
1923            if (mViewStartX > 0) {
1924                xTranslate = mViewWidth;
1925            } else {
1926                xTranslate = -mViewWidth;
1927            }
1928            // Move the canvas around to prep it for the next view
1929            // specifically, shift it by a screen and undo the
1930            // yTranslation which will be redone in the nextView's onDraw().
1931            canvas.translate(xTranslate, -yTranslate);
1932            DayView nextView = (DayView) mViewSwitcher.getNextView();
1933
1934            // Prevent infinite recursive calls to onDraw().
1935            nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
1936
1937            nextView.onDraw(canvas);
1938            // Move it back for this view
1939            canvas.translate(-xTranslate, 0);
1940        } else {
1941            // If we drew another view we already translated it back
1942            // If we didn't draw another view we should be at the edge of the
1943            // screen
1944            canvas.translate(mViewStartX, -yTranslate);
1945        }
1946
1947        // Draw the fixed areas (that don't scroll) directly to the canvas.
1948        drawAfterScroll(canvas);
1949        if (mComputeSelectedEvents && mUpdateToast) {
1950            updateEventDetails();
1951            mUpdateToast = false;
1952        }
1953        mComputeSelectedEvents = false;
1954        canvas.restore();
1955    }
1956
1957    private void drawAfterScroll(Canvas canvas) {
1958        Paint p = mPaint;
1959        Rect r = mRect;
1960
1961        drawAllDayHighlights(r, canvas, p);
1962        if (mMaxAlldayEvents != 0) {
1963            drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p);
1964            drawUpperLeftCorner(r, canvas, p);
1965        }
1966
1967        drawScrollLine(r, canvas, p);
1968        drawDayHeaderLoop(r, canvas, p);
1969
1970        // Draw the AM and PM indicators if we're in 12 hour mode
1971        if (!mIs24HourFormat) {
1972            drawAmPm(canvas, p);
1973        }
1974    }
1975
1976    // This isn't really the upper-left corner. It's the square area just
1977    // below the upper-left corner, above the hours and to the left of the
1978    // all-day area.
1979    private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
1980        // StaticLayout is wonky and doesn't do multi-line center text
1981        // correctly. In order to make it work set the paint to Align.CENTER and
1982        // the Layout to Alignment.ALIGN_NORMAL.
1983        p.setTextAlign(Align.CENTER);
1984        p.setTextSize(ALLDAY_TEXT_SIZE);
1985        p.setColor(mCalendarHourLabelColor);
1986        int translateY = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
1987        r.left = -mHoursWidth / 2;
1988        r.right = mHoursWidth / 2;
1989        r.top = 0;
1990        r.bottom = mFirstCell - translateY;
1991        canvas.save();
1992        canvas.translate(mHoursWidth / 2, translateY);
1993        canvas.clipRect(r);
1994        StaticLayout allDayLayout = getOrCreateAllDayTextLayout(p);
1995        allDayLayout.draw(canvas);
1996        canvas.restore();
1997
1998        setupHourTextPaint(p);
1999        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2000            // Draw the allDay expand/collapse icon
2001            if (mUseExpandIcon) {
2002                mExpandAlldayDrawable.setBounds(mExpandAllDayRect);
2003                mExpandAlldayDrawable.draw(canvas);
2004            } else {
2005                mCollapseAlldayDrawable.setBounds(mExpandAllDayRect);
2006                mCollapseAlldayDrawable.draw(canvas);
2007            }
2008        }
2009    }
2010
2011    private void drawScrollLine(Rect r, Canvas canvas, Paint p) {
2012        final int right = computeDayLeftPosition(mNumDays + 1);
2013        final int y = mFirstCell - 1;
2014
2015        p.setAntiAlias(false);
2016        p.setStyle(Style.FILL);
2017
2018        p.setColor(mCalendarGridLineInnerHorizontalColor);
2019        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2020        canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p);
2021        p.setAntiAlias(true);
2022    }
2023
2024    // Computes the x position for the left side of the given day
2025    private int computeDayLeftPosition(int day) {
2026        int effectiveWidth = mViewWidth - mHoursWidth;
2027        return day * effectiveWidth / mNumDays + mHoursWidth;
2028    }
2029
2030    private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) {
2031        // First, color the labels area light gray
2032        r.top = 0;
2033        r.bottom = DAY_HEADER_HEIGHT;
2034        r.left = 0;
2035        r.right = mViewWidth;
2036        p.setColor(mBgColor);
2037        p.setStyle(Style.FILL);
2038        canvas.drawRect(r, p);
2039        // and the area that says All day
2040        r.top = DAY_HEADER_HEIGHT;
2041        r.bottom = mFirstCell - 1;
2042        r.left = 0;
2043        r.right = mHoursWidth;
2044        canvas.drawRect(r, p);
2045        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2046        if (mNumDays > 1 && todayIndex >= 0 && todayIndex < mNumDays) {
2047            // Draw the today highlight
2048            r.top = 0;
2049            r.bottom = mFirstCell - 1;
2050            r.left = computeDayLeftPosition(todayIndex) + 1;
2051            r.right = computeDayLeftPosition(todayIndex + 1);
2052            p.setColor(mTodayBgColor);
2053            p.setStyle(Style.FILL);
2054            canvas.drawRect(r, p);
2055        }
2056        if (mSelectionAllday) {
2057            // Draw the selection highlight on the selected all-day area
2058            mRect.top = DAY_HEADER_HEIGHT + 1;
2059            mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2;
2060            int daynum = mSelectionDay - mFirstJulianDay;
2061            mRect.left = computeDayLeftPosition(daynum) + 1;
2062            mRect.right = computeDayLeftPosition(daynum + 1);
2063            p.setColor(mCalendarGridAreaSelected);
2064            canvas.drawRect(mRect, p);
2065        }
2066    }
2067
2068    private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
2069        // Draw the horizontal day background banner
2070        // p.setColor(mCalendarDateBannerBackground);
2071        // r.top = 0;
2072        // r.bottom = DAY_HEADER_HEIGHT;
2073        // r.left = 0;
2074        // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
2075        // canvas.drawRect(r, p);
2076        //
2077        // Fill the extra space on the right side with the default background
2078        // r.left = r.right;
2079        // r.right = mViewWidth;
2080        // p.setColor(mCalendarGridAreaBackground);
2081        // canvas.drawRect(r, p);
2082        if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) {
2083            return;
2084        }
2085        int todayNum = mTodayJulianDay - mFirstJulianDay;
2086
2087        p.setTypeface(mBold);
2088        p.setTextAlign(Paint.Align.RIGHT);
2089        int deltaX = mCellWidth + DAY_GAP;
2090        int cell = mFirstJulianDay;
2091
2092        String[] dayNames;
2093        if (mDateStrWidth < mCellWidth) {
2094            dayNames = mDayStrs;
2095        } else {
2096            dayNames = mDayStrs2Letter;
2097        }
2098
2099        p.setAntiAlias(true);
2100        for (int day = 0; day < mNumDays; day++, cell++) {
2101            int dayOfWeek = day + mFirstVisibleDayOfWeek;
2102            if (dayOfWeek >= 14) {
2103                dayOfWeek -= 14;
2104            }
2105
2106            int color = mCalendarDateBannerTextColor;
2107            if (mNumDays == 1) {
2108                if (dayOfWeek == Time.SATURDAY) {
2109                    color = mWeek_saturdayColor;
2110                } else if (dayOfWeek == Time.SUNDAY) {
2111                    color = mWeek_sundayColor;
2112                }
2113            } else {
2114                final int column = day % 7;
2115                if (Utils.isSaturday(column, mFirstDayOfWeek)) {
2116                    color = mWeek_saturdayColor;
2117                } else if (Utils.isSunday(column, mFirstDayOfWeek)) {
2118                    color = mWeek_sundayColor;
2119                }
2120            }
2121
2122            p.setColor(color);
2123            drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p);
2124        }
2125        p.setTypeface(null);
2126    }
2127
2128    private void drawAmPm(Canvas canvas, Paint p) {
2129        p.setColor(mCalendarAmPmLabel);
2130        p.setTextSize(AMPM_TEXT_SIZE);
2131        p.setTypeface(mBold);
2132        p.setAntiAlias(true);
2133        p.setTextAlign(Paint.Align.RIGHT);
2134        String text = mAmString;
2135        if (mFirstHour >= 12) {
2136            text = mPmString;
2137        }
2138        int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
2139        canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2140
2141        if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
2142            // Also draw the "PM"
2143            text = mPmString;
2144            y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
2145                    + 2 * mHoursTextHeight + HOUR_GAP;
2146            canvas.drawText(text, HOURS_LEFT_MARGIN, y, p);
2147        }
2148    }
2149
2150    private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas,
2151            Paint p) {
2152        r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1;
2153        r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER;
2154
2155        r.top = top - mCurrentTimeLine.getIntrinsicHeight() / 2;
2156        r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight();
2157
2158        mCurrentTimeLine.setBounds(r);
2159        mCurrentTimeLine.draw(canvas);
2160    }
2161
2162    private void doDraw(Canvas canvas) {
2163        Paint p = mPaint;
2164        Rect r = mRect;
2165
2166        drawBgColors(r, canvas, p);
2167        drawGridBackground(r, canvas, p);
2168        drawHours(r, canvas, p);
2169
2170        // Draw each day
2171        int cell = mFirstJulianDay;
2172        p.setAntiAlias(false);
2173        for (int day = 0; day < mNumDays; day++, cell++) {
2174            // TODO Wow, this needs cleanup. drawEvents loop through all the
2175            // events on every call.
2176            drawEvents(cell, day, HOUR_GAP, canvas, p);
2177            // If this is today
2178            if (cell == mTodayJulianDay) {
2179                int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2180                        + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2181
2182                // And the current time shows up somewhere on the screen
2183                if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
2184                    drawCurrentTimeLine(r, day, lineY, canvas, p);
2185                }
2186            }
2187        }
2188        p.setAntiAlias(true);
2189    }
2190
2191    private void drawHours(Rect r, Canvas canvas, Paint p) {
2192        // Comment out as the background will be a drawable
2193
2194        // Draw a highlight on the selected hour (if needed)
2195        if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) {
2196            // p.setColor(mCalendarHourSelected);
2197            int daynum = mSelectionDay - mFirstJulianDay;
2198            r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2199            r.bottom = r.top + mCellHeight + HOUR_GAP;
2200            r.left = computeDayLeftPosition(daynum);
2201            r.right = computeDayLeftPosition(daynum + 1);
2202
2203            // Draw a border around the highlighted grid hour.
2204            // drawEmptyRect(canvas, r, mSelectionPaint.getColor());
2205            saveSelectionPosition(r.left, r.top, r.right, r.bottom);
2206
2207            // Also draw the highlight on the grid
2208            p.setColor(mCalendarGridAreaSelected);
2209            r.top += HOUR_GAP;
2210            r.right -= DAY_GAP;
2211            p.setAntiAlias(false);
2212            canvas.drawRect(r, p);
2213        }
2214
2215        setupHourTextPaint(p);
2216
2217        int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN;
2218
2219        for (int i = 0; i < 24; i++) {
2220            String time = mHourStrs[i];
2221            canvas.drawText(time, HOURS_LEFT_MARGIN, y, p);
2222            y += mCellHeight + HOUR_GAP;
2223        }
2224    }
2225
2226    private void setupHourTextPaint(Paint p) {
2227        p.setColor(mCalendarHourLabelColor);
2228        p.setTextSize(HOURS_TEXT_SIZE);
2229        p.setTypeface(Typeface.DEFAULT);
2230        p.setTextAlign(Paint.Align.RIGHT);
2231        p.setAntiAlias(true);
2232    }
2233
2234    private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) {
2235        int dateNum = mFirstVisibleDate + day;
2236        int x;
2237        if (dateNum > mMonthLength) {
2238            dateNum -= mMonthLength;
2239        }
2240        p.setAntiAlias(true);
2241
2242        // Draw day of the month
2243        String dateNumStr = String.valueOf(dateNum);
2244        if (mNumDays > 1) {
2245            float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN;
2246
2247            // Draw day of the month
2248            x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN;
2249            p.setTextAlign(Align.RIGHT);
2250            p.setTextSize(DATE_HEADER_FONT_SIZE);
2251            p.setTypeface(mBold);
2252            canvas.drawText(dateNumStr, x, y, p);
2253
2254            // Draw day of the week
2255            x -= p.measureText(" " + dateNumStr);
2256            p.setTextSize(DAY_HEADER_FONT_SIZE);
2257            p.setTypeface(Typeface.DEFAULT);
2258            canvas.drawText(dayStr, x, y, p);
2259        } else {
2260            float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN;
2261            p.setTextAlign(Align.LEFT);
2262
2263
2264            // Draw day of the week
2265            x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN;
2266            p.setTextSize(DAY_HEADER_FONT_SIZE);
2267            p.setTypeface(Typeface.DEFAULT);
2268            canvas.drawText(dayStr, x, y, p);
2269
2270            // Draw day of the month
2271            x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN;
2272            p.setTextSize(DATE_HEADER_FONT_SIZE);
2273            p.setTypeface(mBold);
2274            canvas.drawText(dateNumStr, x, y, p);
2275        }
2276    }
2277
2278    private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
2279        Paint.Style savedStyle = p.getStyle();
2280
2281        final float stopX = computeDayLeftPosition(mNumDays + 1);
2282        float y = 0;
2283        final float deltaY = mCellHeight + HOUR_GAP;
2284        int linesIndex = 0;
2285        final float startY = 0;
2286        final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
2287        float x = mHoursWidth;
2288
2289        // Draw the inner horizontal grid lines
2290        p.setColor(mCalendarGridLineInnerHorizontalColor);
2291        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2292        p.setAntiAlias(false);
2293        y = 0;
2294        linesIndex = 0;
2295        for (int hour = 0; hour <= 24; hour++) {
2296            mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2297            mLines[linesIndex++] = y;
2298            mLines[linesIndex++] = stopX;
2299            mLines[linesIndex++] = y;
2300            y += deltaY;
2301        }
2302        if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) {
2303            canvas.drawLines(mLines, 0, linesIndex, p);
2304            linesIndex = 0;
2305            p.setColor(mCalendarGridLineInnerVerticalColor);
2306        }
2307
2308        // Draw the inner vertical grid lines
2309        for (int day = 0; day <= mNumDays; day++) {
2310            x = computeDayLeftPosition(day);
2311            mLines[linesIndex++] = x;
2312            mLines[linesIndex++] = startY;
2313            mLines[linesIndex++] = x;
2314            mLines[linesIndex++] = stopY;
2315        }
2316        canvas.drawLines(mLines, 0, linesIndex, p);
2317
2318        // Restore the saved style.
2319        p.setStyle(savedStyle);
2320        p.setAntiAlias(true);
2321    }
2322
2323    /**
2324     * @param r
2325     * @param canvas
2326     * @param p
2327     */
2328    private void drawBgColors(Rect r, Canvas canvas, Paint p) {
2329        int todayIndex = mTodayJulianDay - mFirstJulianDay;
2330        // Draw the hours background color
2331        r.top = mDestRect.top;
2332        r.bottom = mDestRect.bottom;
2333        r.left = 0;
2334        r.right = mHoursWidth;
2335        p.setColor(mBgColor);
2336        p.setStyle(Style.FILL);
2337        p.setAntiAlias(false);
2338        canvas.drawRect(r, p);
2339        if (mNumDays == 1 && todayIndex == 0) {
2340            // Draw a white background for the time later than current time
2341            int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP)
2342                    + ((mCurrentTime.minute * mCellHeight) / 60) + 1;
2343            if (lineY < mViewStartY + mViewHeight) {
2344                lineY = Math.max(lineY, mViewStartY);
2345                r.left = mHoursWidth;
2346                r.right = mViewWidth;
2347                r.top = lineY;
2348                r.bottom = mViewStartY + mViewHeight;
2349                p.setColor(mTodayBgColor);
2350                canvas.drawRect(r, p);
2351            }
2352        } else if (todayIndex >= 0 && todayIndex < mNumDays) {
2353            r.left = computeDayLeftPosition(todayIndex) + 1;
2354            r.right = computeDayLeftPosition(todayIndex + 1);
2355            r.top = mDestRect.top;
2356            r.bottom = mDestRect.bottom;
2357            p.setColor(mTodayBgColor);
2358            canvas.drawRect(r, p);
2359        }
2360        p.setAntiAlias(true);
2361    }
2362
2363    Event getSelectedEvent() {
2364        if (mSelectedEvent == null) {
2365            // There is no event at the selected hour, so create a new event.
2366            return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2367                    getSelectedMinutesSinceMidnight());
2368        }
2369        return mSelectedEvent;
2370    }
2371
2372    boolean isEventSelected() {
2373        return (mSelectedEvent != null);
2374    }
2375
2376    Event getNewEvent() {
2377        return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
2378                getSelectedMinutesSinceMidnight());
2379    }
2380
2381    static Event getNewEvent(int julianDay, long utcMillis,
2382            int minutesSinceMidnight) {
2383        Event event = Event.newInstance();
2384        event.startDay = julianDay;
2385        event.endDay = julianDay;
2386        event.startMillis = utcMillis;
2387        event.endMillis = event.startMillis + MILLIS_PER_HOUR;
2388        event.startTime = minutesSinceMidnight;
2389        event.endTime = event.startTime + MINUTES_PER_HOUR;
2390        return event;
2391    }
2392
2393    private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
2394        float maxWidthF = 0.0f;
2395
2396        int len = strings.length;
2397        for (int i = 0; i < len; i++) {
2398            float width = p.measureText(strings[i]);
2399            maxWidthF = Math.max(width, maxWidthF);
2400        }
2401        int maxWidth = (int) (maxWidthF + 0.5);
2402        if (maxWidth < currentMax) {
2403            maxWidth = currentMax;
2404        }
2405        return maxWidth;
2406    }
2407
2408    private void saveSelectionPosition(float left, float top, float right, float bottom) {
2409        mPrevBox.left = (int) left;
2410        mPrevBox.right = (int) right;
2411        mPrevBox.top = (int) top;
2412        mPrevBox.bottom = (int) bottom;
2413    }
2414
2415    private Rect getCurrentSelectionPosition() {
2416        Rect box = new Rect();
2417        box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
2418        box.bottom = box.top + mCellHeight + HOUR_GAP;
2419        int daynum = mSelectionDay - mFirstJulianDay;
2420        box.left = computeDayLeftPosition(daynum) + 1;
2421        box.right = computeDayLeftPosition(daynum + 1);
2422        return box;
2423    }
2424
2425    private void setupTextRect(Rect r) {
2426        if (r.bottom <= r.top || r.right <= r.left) {
2427            r.bottom = r.top;
2428            r.right = r.left;
2429            return;
2430        }
2431
2432        if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) {
2433            r.top += EVENT_TEXT_TOP_MARGIN;
2434            r.bottom -= EVENT_TEXT_BOTTOM_MARGIN;
2435        }
2436        if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) {
2437            r.left += EVENT_TEXT_LEFT_MARGIN;
2438            r.right -= EVENT_TEXT_RIGHT_MARGIN;
2439        }
2440    }
2441
2442    private void setupAllDayTextRect(Rect r) {
2443        if (r.bottom <= r.top || r.right <= r.left) {
2444            r.bottom = r.top;
2445            r.right = r.left;
2446            return;
2447        }
2448
2449        if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) {
2450            r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN;
2451            r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN;
2452        }
2453        if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) {
2454            r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN;
2455            r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN;
2456        }
2457    }
2458
2459    private StaticLayout getOrCreateAllDayTextLayout(Paint p) {
2460
2461        if (mAllDayTextLayout == null) {
2462            int allDayWidth = mHoursWidth - EVENT_ALL_DAY_TEXT_LEFT_MARGIN
2463                    - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN;
2464            mAllDayTextLayout = new StaticLayout(mAllDayString, 0, mAllDayString.length(),
2465                    new TextPaint(p), allDayWidth, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null,
2466                    allDayWidth);
2467        }
2468        return mAllDayTextLayout;
2469    }
2470
2471    /**
2472     * Return the layout for a numbered event. Create it if not already existing
2473     */
2474    private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint,
2475            Rect r) {
2476        if (i < 0 || i >= layouts.length) {
2477            return null;
2478        }
2479
2480        StaticLayout layout = layouts[i];
2481        // Check if we have already initialized the StaticLayout and that
2482        // the width hasn't changed (due to vertical resizing which causes
2483        // re-layout of events at min height)
2484        if (layout == null || r.width() != layout.getWidth()) {
2485            SpannableStringBuilder bob = new SpannableStringBuilder();
2486            if (event.title != null) {
2487                bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN));
2488                bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0);
2489                bob.append(' ');
2490            }
2491            if (event.location != null) {
2492                bob.append(drawTextSanitizer(event.location.toString(),
2493                        MAX_EVENT_TEXT_LEN - bob.length()));
2494            }
2495
2496            switch (event.selfAttendeeStatus) {
2497                case Attendees.ATTENDEE_STATUS_INVITED:
2498                    paint.setColor(event.color);
2499                    break;
2500                case Attendees.ATTENDEE_STATUS_DECLINED:
2501                case Attendees.ATTENDEE_STATUS_NONE: // Your own events
2502                case Attendees.ATTENDEE_STATUS_ACCEPTED:
2503                case Attendees.ATTENDEE_STATUS_TENTATIVE:
2504                default:
2505                    paint.setColor(mEventTextColor);
2506                    break;
2507            }
2508
2509            // Leave a one pixel boundary on the left and right of the rectangle for the event
2510            layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(),
2511                    Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width());
2512
2513            layouts[i] = layout;
2514        }
2515
2516        return layout;
2517    }
2518
2519    private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) {
2520
2521        p.setTextSize(NORMAL_FONT_SIZE);
2522        p.setTextAlign(Paint.Align.LEFT);
2523        Paint eventTextPaint = mEventTextPaint;
2524
2525        final float startY = DAY_HEADER_HEIGHT;
2526        final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN;
2527        float x = 0;
2528        int linesIndex = 0;
2529
2530        // Draw the inner vertical grid lines
2531        p.setColor(mCalendarGridLineInnerVerticalColor);
2532        x = mHoursWidth;
2533        p.setStrokeWidth(GRID_LINE_INNER_WIDTH);
2534        // Line bounding the top of the all day area
2535        mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN;
2536        mLines[linesIndex++] = startY;
2537        mLines[linesIndex++] = computeDayLeftPosition(mNumDays + 1);
2538        mLines[linesIndex++] = startY;
2539
2540        for (int day = 0; day <= mNumDays; day++) {
2541            x = computeDayLeftPosition(day);
2542            mLines[linesIndex++] = x;
2543            mLines[linesIndex++] = startY;
2544            mLines[linesIndex++] = x;
2545            mLines[linesIndex++] = stopY;
2546        }
2547        p.setAntiAlias(false);
2548        canvas.drawLines(mLines, 0, linesIndex, p);
2549        p.setStyle(Style.FILL);
2550
2551        int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN;
2552        float left = mHoursWidth;
2553        int lastDay = firstDay + numDays - 1;
2554        final ArrayList<Event> events = mAllDayEvents;
2555        int numEvents = events.size();
2556        // Whether or not we should draw the more events text
2557        boolean hasMoreEvents = false;
2558        // size of the allDay area
2559        float drawHeight = mAlldayHeight;
2560        // max number of events being drawn in one day of the allday area
2561        float numRectangles = mMaxAlldayEvents;
2562        // Where to cut off drawn allday events
2563        int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN;
2564        // The number of events that weren't drawn in each day
2565        mSkippedAlldayEvents = new int[numDays];
2566        if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents &&
2567                mAnimateDayHeight == 0) {
2568            // We draw one fewer event than will fit so that more events text
2569            // can be drawn
2570            numRectangles = mMaxUnexpandedAlldayEventCount - 1;
2571            // We also clip the events above the more events text
2572            allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT;
2573            hasMoreEvents = true;
2574        } else if (mAnimateDayHeight != 0) {
2575            // clip at the end of the animating space
2576            allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN;
2577        }
2578        for (int i = 0; i < numEvents; i++) {
2579            Event event = events.get(i);
2580            int startDay = event.startDay;
2581            int endDay = event.endDay;
2582            if (startDay > lastDay || endDay < firstDay) {
2583                continue;
2584            }
2585            if (startDay < firstDay) {
2586                startDay = firstDay;
2587            }
2588            if (endDay > lastDay) {
2589                endDay = lastDay;
2590            }
2591            int startIndex = startDay - firstDay;
2592            int endIndex = endDay - firstDay;
2593            float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight :
2594                    drawHeight / numRectangles;
2595
2596            // Prevent a single event from getting too big
2597            if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
2598                height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
2599            }
2600
2601            // Leave a one-pixel space between the vertical day lines and the
2602            // event rectangle.
2603            event.left = computeDayLeftPosition(startIndex);
2604            event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP;
2605            event.top = y + height * event.getColumn();
2606            event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN;
2607            if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
2608                // check if we should skip this event. We skip if it starts
2609                // after the clip bound or ends after the skip bound and we're
2610                // not animating.
2611                if (event.top >= allDayEventClip) {
2612                    incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2613                    continue;
2614                } else if (event.bottom > allDayEventClip) {
2615                    if (hasMoreEvents) {
2616                        incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex);
2617                        continue;
2618                    }
2619                    event.bottom = allDayEventClip;
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        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            events = mAllDayEvents;
4168            numEvents = events.size();
4169            for (int i = 0; i < numEvents; i++) {
4170                Event event = events.get(i);
4171                if (!event.drawAsAllday() ||
4172                        (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) {
4173                    // Don't check non-allday events or events that aren't shown
4174                    continue;
4175                }
4176
4177                if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
4178                    float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents
4179                            : mMaxUnexpandedAlldayEventCount;
4180                    float height = drawHeight / numRectangles;
4181                    if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
4182                        height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT;
4183                    }
4184                    float eventTop = yOffset + height * event.getColumn();
4185                    float eventBottom = eventTop + height;
4186                    if (eventTop < y && eventBottom > y) {
4187                        // If the touch is inside the event rectangle, then
4188                        // add the event.
4189                        mSelectedEvents.add(event);
4190                        closestEvent = event;
4191                        break;
4192                    } else {
4193                        // Find the closest event
4194                        if (eventTop >= y) {
4195                            yDistance = eventTop - y;
4196                        } else {
4197                            yDistance = y - eventBottom;
4198                        }
4199                        if (yDistance < minYdistance) {
4200                            minYdistance = yDistance;
4201                            closestEvent = event;
4202                        }
4203                    }
4204                }
4205            }
4206            mSelectedEvent = closestEvent;
4207            return;
4208        }
4209
4210        // Adjust y for the scrollable bitmap
4211        y += mViewStartY - mFirstCell;
4212
4213        // Use a region around (x,y) for the selection region
4214        Rect region = mRect;
4215        region.left = x - 10;
4216        region.right = x + 10;
4217        region.top = y - 10;
4218        region.bottom = y + 10;
4219
4220        EventGeometry geometry = mEventGeometry;
4221
4222        for (int i = 0; i < numEvents; i++) {
4223            Event event = events.get(i);
4224            // Compute the event rectangle.
4225            if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
4226                continue;
4227            }
4228
4229            // If the event intersects the selection region, then add it to
4230            // mSelectedEvents.
4231            if (geometry.eventIntersectsSelection(event, region)) {
4232                mSelectedEvents.add(event);
4233            }
4234        }
4235
4236        // If there are any events in the selected region, then assign the
4237        // closest one to mSelectedEvent.
4238        if (mSelectedEvents.size() > 0) {
4239            int len = mSelectedEvents.size();
4240            Event closestEvent = null;
4241            float minDist = mViewWidth + mViewHeight; // some large distance
4242            for (int index = 0; index < len; index++) {
4243                Event ev = mSelectedEvents.get(index);
4244                float dist = geometry.pointToEvent(x, y, ev);
4245                if (dist < minDist) {
4246                    minDist = dist;
4247                    closestEvent = ev;
4248                }
4249            }
4250            mSelectedEvent = closestEvent;
4251
4252            // Keep the selected hour and day consistent with the selected
4253            // event. They could be different if we touched on an empty hour
4254            // slot very close to an event in the previous hour slot. In
4255            // that case we will select the nearby event.
4256            int startDay = mSelectedEvent.startDay;
4257            int endDay = mSelectedEvent.endDay;
4258            if (mSelectionDay < startDay) {
4259                mSelectionDay = startDay;
4260            } else if (mSelectionDay > endDay) {
4261                mSelectionDay = endDay;
4262            }
4263
4264            int startHour = mSelectedEvent.startTime / 60;
4265            int endHour;
4266            if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
4267                endHour = (mSelectedEvent.endTime - 1) / 60;
4268            } else {
4269                endHour = mSelectedEvent.endTime / 60;
4270            }
4271
4272            if (mSelectionHour < startHour && mSelectionDay == startDay) {
4273                mSelectionHour = startHour;
4274            } else if (mSelectionHour > endHour && mSelectionDay == endDay) {
4275                mSelectionHour = endHour;
4276            }
4277        }
4278    }
4279
4280    // Encapsulates the code to continue the scrolling after the
4281    // finger is lifted. Instead of stopping the scroll immediately,
4282    // the scroll continues to "free spin" and gradually slows down.
4283    private class ContinueScroll implements Runnable {
4284        public void run() {
4285            mScrolling = mScrolling && mScroller.computeScrollOffset();
4286            if (!mScrolling) {
4287                resetSelectedHour();
4288                invalidate();
4289                return;
4290            }
4291
4292            mViewStartY = mScroller.getCurrY();
4293
4294            if (mViewStartY < 0) {
4295                mViewStartY = 0;
4296            } else if (mViewStartY > mMaxViewStartY) {
4297                mViewStartY = mMaxViewStartY;
4298            }
4299
4300            computeFirstHour();
4301            post(this);
4302            invalidate();
4303        }
4304    }
4305
4306    /**
4307     * Cleanup the pop-up and timers.
4308     */
4309    public void cleanup() {
4310        // Protect against null-pointer exceptions
4311        if (mPopup != null) {
4312            mPopup.dismiss();
4313        }
4314        mLastPopupEventID = INVALID_EVENT_ID;
4315        Handler handler = getHandler();
4316        if (handler != null) {
4317            handler.removeCallbacks(mDismissPopup);
4318            handler.removeCallbacks(mUpdateCurrentTime);
4319        }
4320
4321        Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT,
4322            mCellHeight);
4323
4324        // Turn off redraw
4325        mRemeasure = false;
4326    }
4327
4328    /**
4329     * Restart the update timer
4330     */
4331    public void restartCurrentTimeUpdates() {
4332        post(mUpdateCurrentTime);
4333    }
4334
4335    @Override
4336    protected void onDetachedFromWindow() {
4337        cleanup();
4338        super.onDetachedFromWindow();
4339    }
4340
4341    class DismissPopup implements Runnable {
4342        public void run() {
4343            // Protect against null-pointer exceptions
4344            if (mPopup != null) {
4345                mPopup.dismiss();
4346            }
4347        }
4348    }
4349
4350    class UpdateCurrentTime implements Runnable {
4351        public void run() {
4352            long currentTime = System.currentTimeMillis();
4353            mCurrentTime.set(currentTime);
4354            //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
4355            postDelayed(mUpdateCurrentTime,
4356                    UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
4357            mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
4358            invalidate();
4359        }
4360    }
4361
4362    class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
4363        @Override
4364        public boolean onSingleTapUp(MotionEvent ev) {
4365            DayView.this.doSingleTapUp(ev);
4366            return true;
4367        }
4368
4369        @Override
4370        public void onLongPress(MotionEvent ev) {
4371            DayView.this.doLongPress(ev);
4372        }
4373
4374        @Override
4375        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
4376            if (mTouchStartedInAlldayArea) {
4377                if (Math.abs(distanceX) < Math.abs(distanceY)) {
4378                    return false;
4379                }
4380                // don't scroll vertically if this started in the allday area
4381                distanceY = 0;
4382            }
4383            DayView.this.doScroll(e1, e2, distanceX, distanceY);
4384            return true;
4385        }
4386
4387        @Override
4388        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
4389            if (mTouchStartedInAlldayArea) {
4390                if (Math.abs(velocityX) < Math.abs(velocityY)) {
4391                    return false;
4392                }
4393                // don't fling vertically if this started in the allday area
4394                velocityY = 0;
4395            }
4396            DayView.this.doFling(e1, e2, velocityX, velocityY);
4397            return true;
4398        }
4399
4400        @Override
4401        public boolean onDown(MotionEvent ev) {
4402            DayView.this.doDown(ev);
4403            return true;
4404        }
4405    }
4406
4407    @Override
4408    public boolean onLongClick(View v) {
4409        mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1,
4410                getSelectedTimeInMillis(), 0, -1, -1, -1);
4411        return true;
4412    }
4413
4414    // The rest of this file was borrowed from Launcher2 - PagedView.java
4415    private static final int MINIMUM_SNAP_VELOCITY = 2200;
4416
4417    private class ScrollInterpolator implements Interpolator {
4418        public ScrollInterpolator() {
4419        }
4420
4421        public float getInterpolation(float t) {
4422            t -= 1.0f;
4423            t = t * t * t * t * t + 1;
4424
4425            if ((1 - t) * mAnimationDistance < 1) {
4426                cancelAnimation();
4427            }
4428
4429            return t;
4430        }
4431    }
4432
4433    private long calculateDuration(float delta, float width, float velocity) {
4434        /*
4435         * Here we compute a "distance" that will be used in the computation of
4436         * the overall snap duration. This is a function of the actual distance
4437         * that needs to be traveled; we keep this value close to half screen
4438         * size in order to reduce the variance in snap duration as a function
4439         * of the distance the page needs to travel.
4440         */
4441        final float halfScreenSize = width / 2;
4442        float distanceRatio = delta / width;
4443        float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio);
4444        float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration;
4445
4446        velocity = Math.abs(velocity);
4447        velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity);
4448
4449        /*
4450         * we want the page's snap velocity to approximately match the velocity
4451         * at which the user flings, so we scale the duration by a value near to
4452         * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to
4453         * make it a little slower.
4454         */
4455        long duration = 6 * Math.round(1000 * Math.abs(distance / velocity));
4456        if (DEBUG) {
4457            Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:"
4458                    + distanceRatio + " distance:" + distance + " velocity:" + velocity
4459                    + " duration:" + duration + " distanceInfluenceForSnapDuration:"
4460                    + distanceInfluenceForSnapDuration);
4461        }
4462        return duration;
4463    }
4464
4465    /*
4466     * We want the duration of the page snap animation to be influenced by the
4467     * distance that the screen has to travel, however, we don't want this
4468     * duration to be effected in a purely linear fashion. Instead, we use this
4469     * method to moderate the effect that the distance of travel has on the
4470     * overall snap duration.
4471     */
4472    private float distanceInfluenceForSnapDuration(float f) {
4473        f -= 0.5f; // center the values about 0.
4474        f *= 0.3f * Math.PI / 2.0f;
4475        return (float) Math.sin(f);
4476    }
4477}
4478