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