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