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