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