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