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