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