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