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