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