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