2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.calendar;
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.database.Cursor;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Paint;
33 import android.graphics.Path;
34 import android.graphics.PorterDuff;
35 import android.graphics.Rect;
36 import android.graphics.RectF;
37 import android.graphics.Typeface;
38 import android.graphics.Paint.Style;
39 import android.graphics.Path.Direction;
40 import android.net.Uri;
41 import android.os.Handler;
42 import android.provider.Calendar.Attendees;
43 import android.provider.Calendar.Calendars;
44 import android.provider.Calendar.Events;
45 import android.text.format.DateFormat;
46 import android.text.format.DateUtils;
47 import android.text.format.Time;
48 import android.text.TextUtils;
49 import android.util.Log;
50 import android.view.ContextMenu;
51 import android.view.Gravity;
52 import android.view.KeyEvent;
53 import android.view.LayoutInflater;
54 import android.view.MenuItem;
55 import android.view.MotionEvent;
56 import android.view.View;
57 import android.view.ViewConfiguration;
58 import android.view.ViewGroup;
59 import android.view.WindowManager;
60 import android.view.ContextMenu.ContextMenuInfo;
61 import android.widget.ImageView;
62 import android.widget.PopupWindow;
63 import android.widget.TextView;
65 import java.util.ArrayList;
66 import java.util.Calendar;
67 import java.util.regex.Matcher;
68 import java.util.regex.Pattern;
71 * This is the base class for a set of classes that implement views (day view
72 * and week view to start with) that share some common code.
74 public class CalendarView extends View
75 implements View.OnCreateContextMenuListener, View.OnClickListener {
77 private static float mScale = 0; // Used for supporting different screen densities
79 private boolean mOnFlingCalled;
81 protected CalendarApplication mCalendarApp;
82 protected CalendarActivity mParentActivity;
84 private static final String[] CALENDARS_PROJECTION = new String[] {
86 Calendars.ACCESS_LEVEL, // 1
88 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
89 private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
91 private static final String[] ATTENDEES_PROJECTION = new String[] {
93 Attendees.ATTENDEE_RELATIONSHIP, // 1
95 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
96 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
98 private static float SMALL_ROUND_RADIUS = 3.0F;
100 private static final int FROM_NONE = 0;
101 private static final int FROM_ABOVE = 1;
102 private static final int FROM_BELOW = 2;
103 private static final int FROM_LEFT = 4;
104 private static final int FROM_RIGHT = 8;
106 private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
108 private ContinueScroll mContinueScroll = new ContinueScroll();
110 static private class DayHeader{
115 private DayHeader[] dayHeaders = new DayHeader[32];
117 // Make this visible within the package for more informative debugging
120 private Typeface mBold = Typeface.DEFAULT_BOLD;
121 private int mFirstJulianDay;
122 private int mLastJulianDay;
124 private int mMonthLength;
125 private int mFirstDate;
126 private int[] mEarliestStartHour; // indexed by the week day offset
127 private boolean[] mHasAllDayEvent; // indexed by the week day offset
129 private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
132 * This variable helps to avoid unnecessarily reloading events by keeping
133 * track of the start millis parameter used for the most recent loading
134 * of events. If the next reload matches this, then the events are not
135 * reloaded. To force a reload, set this to zero (this is set to zero
136 * in the method clearCachedEvents()).
138 private long mLastReloadMillis;
140 private ArrayList<Event> mEvents = new ArrayList<Event>();
141 private int mSelectionDay; // Julian day
142 private int mSelectionHour;
144 /* package private so that CalendarActivity can read it when creating new
147 boolean mSelectionAllDay;
149 private int mCellWidth;
150 private boolean mLaunchNewView;
152 // Pre-allocate these objects and re-use them
153 private Rect mRect = new Rect();
154 private RectF mRectF = new RectF();
155 private Rect mSrcRect = new Rect();
156 private Rect mDestRect = new Rect();
157 private Paint mPaint = new Paint();
158 private Paint mPaintBorder = new Paint();
159 private Paint mEventTextPaint = new Paint();
160 private Paint mSelectionPaint = new Paint();
161 private Path mPath = new Path();
163 protected boolean mDrawTextInEventRect;
164 private int mStartDay;
166 private PopupWindow mPopup;
167 private View mPopupView;
169 // The number of milliseconds to show the popup window
170 private static final int POPUP_DISMISS_DELAY = 3000;
171 private DismissPopup mDismissPopup = new DismissPopup();
173 // For drawing to an off-screen Canvas
174 private Bitmap mBitmap;
175 private Canvas mCanvas;
176 private boolean mRedrawScreen = true;
177 private boolean mRemeasure = true;
179 private final EventLoader mEventLoader;
180 protected final EventGeometry mEventGeometry;
182 private static final int DAY_GAP = 1;
183 private static final int HOUR_GAP = 1;
184 private static int SINGLE_ALLDAY_HEIGHT = 20;
185 private static int MAX_ALLDAY_HEIGHT = 72;
186 private static int ALLDAY_TOP_MARGIN = 3;
187 private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
189 /* The extra space to leave above the text in all-day events */
190 private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
192 /* The extra space to leave above the text in normal events */
193 private static final int NORMAL_TEXT_TOP_MARGIN = 2;
195 private static final int HOURS_LEFT_MARGIN = 2;
196 private static final int HOURS_RIGHT_MARGIN = 4;
197 private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
199 /* package */ static final int MINUTES_PER_HOUR = 60;
200 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
201 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
202 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
203 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
205 private static int NORMAL_FONT_SIZE = 12;
206 private static int EVENT_TEXT_FONT_SIZE = 12;
207 private static int HOURS_FONT_SIZE = 12;
208 private static int AMPM_FONT_SIZE = 9;
209 private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
210 private static final int MAX_EVENT_TEXT_LEN = 500;
211 private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels
213 private static int mSelectionColor;
214 private static int mPressedColor;
215 private static int mSelectedEventTextColor;
216 private static int mEventTextColor;
217 private static int mWeek_weekendColor;
218 private static int mCalendarDateBannerTextColor;
219 private static int mCalendarAllDayBackground;
220 private static int mCalendarAmPmLabel;
221 private static int mCalendarDateBannerBackground;
222 private static int mCalendarDateSelected;
223 private static int mCalendarGridAreaBackground;
224 private static int mCalendarGridAreaSelected;
225 private static int mCalendarGridLineHorizontalColor;
226 private static int mCalendarGridLineVerticalColor;
227 private static int mCalendarHourBackground;
228 private static int mCalendarHourLabel;
229 private static int mCalendarHourSelected;
231 private int mViewStartX;
232 private int mViewStartY;
233 private int mMaxViewStartY;
234 private int mBitmapHeight;
235 private int mViewHeight;
236 private int mViewWidth;
237 private int mGridAreaHeight;
238 private int mCellHeight;
239 private int mScrollStartY;
240 private int mPreviousDirection;
241 private int mPreviousDistanceX;
243 private int mHoursTextHeight;
244 private int mEventTextAscent;
245 private int mEventTextHeight;
246 private int mAllDayHeight;
247 private int mBannerPlusMargin;
248 private int mMaxAllDayEvents;
250 protected int mNumDays = 7;
251 private int mNumHours = 10;
252 private int mHoursWidth;
253 private int mDateStrWidth;
254 private int mFirstCell;
255 private int mFirstHour = -1;
256 private int mFirstHourOffset;
257 private String[] mHourStrs;
258 private String[] mDayStrs;
259 private String[] mDayStrs2Letter;
260 private boolean mIs24HourFormat;
262 private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
263 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
264 private boolean mComputeSelectedEvents;
265 private Event mSelectedEvent;
266 private Event mPrevSelectedEvent;
267 private Rect mPrevBox = new Rect();
268 protected final Resources mResources;
269 private String mAmString;
270 private String mPmString;
271 private DeleteEventHelper mDeleteEventHelper;
273 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
276 * The initial state of the touch mode when we enter this view.
278 private static final int TOUCH_MODE_INITIAL_STATE = 0;
281 * Indicates we just received the touch event and we are waiting to see if
282 * it is a tap or a scroll gesture.
284 private static final int TOUCH_MODE_DOWN = 1;
287 * Indicates the touch gesture is a vertical scroll
289 private static final int TOUCH_MODE_VSCROLL = 0x20;
292 * Indicates the touch gesture is a horizontal scroll
294 private static final int TOUCH_MODE_HSCROLL = 0x40;
296 private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
299 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
301 private static final int SELECTION_HIDDEN = 0;
302 private static final int SELECTION_PRESSED = 1;
303 private static final int SELECTION_SELECTED = 2;
304 private static final int SELECTION_LONGPRESS = 3;
306 private int mSelectionMode = SELECTION_HIDDEN;
308 private boolean mScrolling = false;
310 private String mDateRange;
311 private TextView mTitleTextView;
313 public CalendarView(CalendarActivity activity) {
316 mScale = getContext().getResources().getDisplayMetrics().density;
318 SINGLE_ALLDAY_HEIGHT *= mScale;
319 MAX_ALLDAY_HEIGHT *= mScale;
320 ALLDAY_TOP_MARGIN *= mScale;
321 MAX_ALLDAY_EVENT_HEIGHT *= mScale;
323 NORMAL_FONT_SIZE *= mScale;
324 EVENT_TEXT_FONT_SIZE *= mScale;
325 HOURS_FONT_SIZE *= mScale;
326 AMPM_FONT_SIZE *= mScale;
327 MIN_CELL_WIDTH_FOR_TEXT *= mScale;
328 MIN_EVENT_HEIGHT *= mScale;
330 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
332 SMALL_ROUND_RADIUS *= mScale;
336 mResources = activity.getResources();
337 mEventLoader = activity.mEventLoader;
338 mEventGeometry = new EventGeometry();
339 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
340 mEventGeometry.setHourGap(HOUR_GAP);
341 mParentActivity = activity;
342 mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
343 mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
348 private void init(Context context) {
351 // Allow focus in touch mode so that we can do keyboard shortcuts
352 // even after we've entered touch mode.
353 setFocusableInTouchMode(true);
355 setOnCreateContextMenuListener(this);
357 mStartDay = Calendar.getInstance().getFirstDayOfWeek();
358 if (mStartDay == Calendar.SATURDAY) {
359 mStartDay = Time.SATURDAY;
360 } else if (mStartDay == Calendar.MONDAY) {
361 mStartDay = Time.MONDAY;
363 mStartDay = Time.SUNDAY;
366 mWeek_weekendColor = mResources.getColor(R.color.week_weekend);
367 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
368 mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
369 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
370 mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
371 mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
372 mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
373 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
374 mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
375 mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
376 mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
377 mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
378 mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
379 mSelectionColor = mResources.getColor(R.color.selection);
380 mPressedColor = mResources.getColor(R.color.pressed);
381 mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
382 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
383 mEventTextPaint.setColor(mEventTextColor);
384 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
385 mEventTextPaint.setTextAlign(Paint.Align.LEFT);
386 mEventTextPaint.setAntiAlias(true);
388 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
389 Paint p = mSelectionPaint;
390 p.setColor(gridLineColor);
391 p.setStyle(Style.STROKE);
392 p.setStrokeWidth(2.0f);
393 p.setAntiAlias(false);
396 p.setAntiAlias(true);
398 mPaintBorder.setColor(0xffc8c8c8);
399 mPaintBorder.setStyle(Style.STROKE);
400 mPaintBorder.setAntiAlias(true);
401 mPaintBorder.setStrokeWidth(2.0f);
403 // Allocate space for 2 weeks worth of weekday names so that we can
404 // easily start the week display at any week day.
405 mDayStrs = new String[14];
407 // Also create an array of 2-letter abbreviations.
408 mDayStrs2Letter = new String[14];
410 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
411 int index = i - Calendar.SUNDAY;
412 // e.g. Tue for Tuesday
413 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
414 mDayStrs[index + 7] = mDayStrs[index];
415 // e.g. Tu for Tuesday
416 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
418 // If we don't have 2-letter day strings, fall back to 1-letter.
419 if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
420 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
423 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
426 // Figure out how much space we need for the 3-letter abbrev names
427 // in the worst case.
428 p.setTextSize(NORMAL_FONT_SIZE);
429 p.setTypeface(mBold);
430 String[] dateStrs = {" 28", " 30"};
431 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
432 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
434 p.setTextSize(HOURS_FONT_SIZE);
436 mIs24HourFormat = DateFormat.is24HourFormat(context);
437 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
438 mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
440 mAmString = DateUtils.getAMPMString(Calendar.AM);
441 mPmString = DateUtils.getAMPMString(Calendar.PM);
442 String[] ampm = {mAmString, mPmString};
443 p.setTextSize(AMPM_FONT_SIZE);
444 mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
445 mHoursWidth += HOURS_MARGIN;
447 LayoutInflater inflater;
448 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
449 mPopupView = inflater.inflate(R.layout.bubble_event, null);
450 mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
451 ViewGroup.LayoutParams.FILL_PARENT,
452 ViewGroup.LayoutParams.WRAP_CONTENT));
453 mPopup = new PopupWindow(context);
454 mPopup.setContentView(mPopupView);
455 Resources.Theme dialogTheme = getResources().newTheme();
456 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
457 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
458 android.R.attr.windowBackground });
459 mPopup.setBackgroundDrawable(ta.getDrawable(0));
462 // Enable touching the popup window
463 mPopupView.setOnClickListener(this);
465 mBaseDate = new Time();
466 long millis = System.currentTimeMillis();
467 mBaseDate.set(millis);
469 mEarliestStartHour = new int[mNumDays];
470 mHasAllDayEvent = new boolean[mNumDays];
472 mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
473 mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
477 * This is called when the popup window is pressed.
479 public void onClick(View v) {
480 if (v == mPopupView) {
481 // Pretend it was a trackball click because that will always
482 // jump to the "View event" screen.
483 switchViews(true /* trackball */);
488 * Returns the start of the selected time in milliseconds since the epoch.
490 * @return selected time in UTC milliseconds since the epoch.
492 long getSelectedTimeInMillis() {
493 Time time = new Time(mBaseDate);
494 time.setJulianDay(mSelectionDay);
495 time.hour = mSelectionHour;
497 // We ignore the "isDst" field because we want normalize() to figure
498 // out the correct DST value and not adjust the selected time based
499 // on the current setting of DST.
500 return time.normalize(true /* ignore isDst */);
503 Time getSelectedTime() {
504 Time time = new Time(mBaseDate);
505 time.setJulianDay(mSelectionDay);
506 time.hour = mSelectionHour;
508 // We ignore the "isDst" field because we want normalize() to figure
509 // out the correct DST value and not adjust the selected time based
510 // on the current setting of DST.
511 time.normalize(true /* ignore isDst */);
516 * Returns the start of the selected time in minutes since midnight,
517 * local time. The derived class must ensure that this is consistent
518 * with the return value from getSelectedTimeInMillis().
520 int getSelectedMinutesSinceMidnight() {
521 return mSelectionHour * MINUTES_PER_HOUR;
524 public void setSelectedDay(Time time) {
526 mSelectionHour = mBaseDate.hour;
527 mSelectedEvent = null;
528 mPrevSelectedEvent = null;
529 long millis = mBaseDate.toMillis(false /* use isDst */);
530 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
531 mSelectedEvents.clear();
532 mComputeSelectedEvents = true;
534 // Force a recalculation of the first visible hour
537 mTitleTextView.setText(mDateRange);
539 // Force a redraw of the selection box.
540 mSelectionMode = SELECTION_SELECTED;
541 mRedrawScreen = true;
546 public Time getSelectedDay() {
547 Time time = new Time(mBaseDate);
548 time.setJulianDay(mSelectionDay);
549 time.hour = mSelectionHour;
551 // We ignore the "isDst" field because we want normalize() to figure
552 // out the correct DST value and not adjust the selected time based
553 // on the current setting of DST.
554 time.normalize(true /* ignore isDst */);
558 private void recalc() {
559 // Set the base date to the beginning of the week if we are displaying
562 int dayOfWeek = mBaseDate.weekDay;
563 int diff = dayOfWeek - mStartDay;
568 mBaseDate.monthDay -= diff;
569 mBaseDate.normalize(true /* ignore isDst */);
573 final long start = mBaseDate.toMillis(false /* use isDst */);
575 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
576 mLastJulianDay = mFirstJulianDay + mNumDays - 1;
578 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
579 mFirstDate = mBaseDate.monthDay;
581 int flags = DateUtils.FORMAT_SHOW_YEAR;
582 if (DateFormat.is24HourFormat(mContext)) {
583 flags |= DateUtils.FORMAT_24HOUR;
586 mBaseDate.monthDay += mNumDays - 1;
587 end = mBaseDate.toMillis(true /* ignore isDst */);
588 mBaseDate.monthDay -= mNumDays - 1;
589 flags |= DateUtils.FORMAT_NO_MONTH_DAY;
591 flags |= DateUtils.FORMAT_SHOW_WEEKDAY
592 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
595 mDateRange = DateUtils.formatDateRange(mParentActivity, start, end, flags);
596 // Do not set the title here because this is called when executing
597 // initNextView() to prepare the Day view when sliding the finger
598 // horizontally but we don't always want to change the title. And
599 // if we change the title here and then change it back in the caller
600 // then we get an annoying flicker.
603 void setDetailedView(String detailedView) {
604 mDetailedView = detailedView;
608 protected void onSizeChanged(int width, int height, int oldw, int oldh) {
610 mViewHeight = height;
611 int gridAreaWidth = width - mHoursWidth;
612 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
614 Paint p = new Paint();
615 p.setTextSize(NORMAL_FONT_SIZE);
616 int bannerTextHeight = (int) Math.abs(p.ascent());
618 p.setTextSize(HOURS_FONT_SIZE);
619 mHoursTextHeight = (int) Math.abs(p.ascent());
621 p.setTextSize(EVENT_TEXT_FONT_SIZE);
622 float ascent = -p.ascent();
623 mEventTextAscent = (int) Math.ceil(ascent);
624 float totalHeight = ascent + p.descent();
625 mEventTextHeight = (int) Math.ceil(totalHeight);
628 mBannerPlusMargin = bannerTextHeight + 14;
630 mBannerPlusMargin = 0;
633 remeasure(width, height);
636 // Measures the space needed for various parts of the view after
637 // loading new events. This can change if there are all-day events.
638 private void remeasure(int width, int height) {
640 // First, clear the array of earliest start times, and the array
641 // indicating presence of an all-day event.
642 for (int day = 0; day < mNumDays; day++) {
643 mEarliestStartHour[day] = 25; // some big number
644 mHasAllDayEvent[day] = false;
647 // Compute the space needed for the all-day events, if any.
648 // Make a pass over all the events, and keep track of the maximum
649 // number of all-day events in any one day. Also, keep track of
650 // the earliest event in each day.
651 int maxAllDayEvents = 0;
652 ArrayList<Event> events = mEvents;
653 int len = events.size();
654 for (int ii = 0; ii < len; ii++) {
655 Event event = events.get(ii);
656 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
659 int max = event.getColumn() + 1;
660 if (maxAllDayEvents < max) {
661 maxAllDayEvents = max;
663 int daynum = event.startDay - mFirstJulianDay;
664 int durationDays = event.endDay - event.startDay + 1;
666 durationDays += daynum;
669 if (daynum + durationDays > mNumDays) {
670 durationDays = mNumDays - daynum;
672 for (int day = daynum; durationDays > 0; day++, durationDays--) {
673 mHasAllDayEvent[day] = true;
676 int daynum = event.startDay - mFirstJulianDay;
677 int hour = event.startTime / 60;
678 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
679 mEarliestStartHour[daynum] = hour;
682 // Also check the end hour in case the event spans more than
684 daynum = event.endDay - mFirstJulianDay;
685 hour = event.endTime / 60;
686 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
687 mEarliestStartHour[daynum] = hour;
691 mMaxAllDayEvents = maxAllDayEvents;
693 mFirstCell = mBannerPlusMargin;
694 int allDayHeight = 0;
695 if (maxAllDayEvents > 0) {
696 // If there is at most one all-day event per day, then use less
697 // space (but more than the space for a single event).
698 if (maxAllDayEvents == 1) {
699 allDayHeight = SINGLE_ALLDAY_HEIGHT;
701 // Allow the all-day area to grow in height depending on the
702 // number of all-day events we need to show, up to a limit.
703 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
704 if (allDayHeight > MAX_ALLDAY_HEIGHT) {
705 allDayHeight = MAX_ALLDAY_HEIGHT;
708 mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
710 mSelectionAllDay = false;
712 mAllDayHeight = allDayHeight;
714 mGridAreaHeight = height - mFirstCell;
715 mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
716 int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
717 int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
718 mEventGeometry.setHourHeight(mCellHeight);
720 // Create an off-screen bitmap that we can draw into.
721 mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
722 if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
724 if (mBitmap != null) {
727 mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
728 mCanvas = new Canvas(mBitmap);
730 mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
732 if (mFirstHour == -1) {
734 mFirstHourOffset = 0;
737 // When we change the base date, the number of all-day events may
738 // change and that changes the cell height. When we switch dates,
739 // we use the mFirstHourOffset from the previous view, but that may
740 // be too large for the new view if the cell height is smaller.
741 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
742 mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
744 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
746 int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
748 mPopup.setWidth(eventAreaWidth - 20);
749 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
753 * Initialize the state for another view. The given view is one that has
754 * its own bitmap and will use an animation to replace the current view.
755 * The current view and new view are either both Week views or both Day
756 * views. They differ in their base date.
758 * @param view the view to initialize.
760 private void initView(CalendarView view) {
761 view.mSelectionHour = mSelectionHour;
762 view.mSelectedEvents.clear();
763 view.mComputeSelectedEvents = true;
764 view.mFirstHour = mFirstHour;
765 view.mFirstHourOffset = mFirstHourOffset;
766 view.remeasure(getWidth(), getHeight());
768 view.mSelectedEvent = null;
769 view.mPrevSelectedEvent = null;
770 view.mStartDay = mStartDay;
771 if (view.mEvents.size() > 0) {
772 view.mSelectionAllDay = mSelectionAllDay;
774 view.mSelectionAllDay = false;
777 // Redraw the screen so that the selection box will be redrawn. We may
778 // have scrolled to a different part of the day in some other view
779 // so the selection box in this view may no longer be visible.
780 view.mRedrawScreen = true;
785 * Switch to another view based on what was selected (an event or a free
786 * slot) and how it was selected (by touch or by trackball).
788 * @param trackBallSelection true if the selection was made using the
791 private void switchViews(boolean trackBallSelection) {
792 Event selectedEvent = mSelectedEvent;
796 // This is the Week view.
797 // With touch, we always switch to Day/Agenda View
798 // With track ball, if we selected a free slot, then create an event.
799 // If we selected a specific event, switch to EventInfo view.
800 if (trackBallSelection) {
801 if (selectedEvent == null) {
802 // Switch to the EditEvent view
803 long startMillis = getSelectedTimeInMillis();
804 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
805 Intent intent = new Intent(Intent.ACTION_VIEW);
806 intent.setClassName(mContext, EditEvent.class.getName());
807 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
808 intent.putExtra(EVENT_END_TIME, endMillis);
809 mParentActivity.startActivity(intent);
811 // Switch to the EventInfo view
812 Intent intent = new Intent(Intent.ACTION_VIEW);
813 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
815 intent.setData(eventUri);
816 intent.setClassName(mContext, EventInfoActivity.class.getName());
817 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
818 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
819 mParentActivity.startActivity(intent);
822 // This was a touch selection. If the touch selected a single
823 // unambiguous event, then view that event. Otherwise go to
825 if (mSelectedEvents.size() == 1) {
826 // Switch to the EventInfo view
827 Intent intent = new Intent(Intent.ACTION_VIEW);
828 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
830 intent.setData(eventUri);
831 intent.setClassName(mContext, EventInfoActivity.class.getName());
832 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
833 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
834 mParentActivity.startActivity(intent);
836 // Switch to the Day/Agenda view.
837 long millis = getSelectedTimeInMillis();
838 Utils.startActivity(mParentActivity, mDetailedView, millis);
842 // This is the Day view.
843 // If we selected a free slot, then create an event.
844 // If we selected an event, then go to the EventInfo view.
845 if (selectedEvent == null) {
846 // Switch to the EditEvent view
847 long startMillis = getSelectedTimeInMillis();
848 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
849 Intent intent = new Intent(Intent.ACTION_VIEW);
850 intent.setClassName(mContext, EditEvent.class.getName());
851 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
852 intent.putExtra(EVENT_END_TIME, endMillis);
853 mParentActivity.startActivity(intent);
855 // Switch to the EventInfo view
856 Intent intent = new Intent(Intent.ACTION_VIEW);
857 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
858 intent.setData(eventUri);
859 intent.setClassName(mContext, EventInfoActivity.class.getName());
860 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
861 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
862 mParentActivity.startActivity(intent);
868 public boolean onKeyUp(int keyCode, KeyEvent event) {
870 long duration = event.getEventTime() - event.getDownTime();
873 case KeyEvent.KEYCODE_DPAD_CENTER:
874 if (mSelectionMode == SELECTION_HIDDEN) {
875 // Don't do anything unless the selection is visible.
879 if (mSelectionMode == SELECTION_PRESSED) {
880 // This was the first press when there was nothing selected.
881 // Change the selection from the "pressed" state to the
882 // the "selected" state. We treat short-press and
883 // long-press the same here because nothing was selected.
884 mSelectionMode = SELECTION_SELECTED;
885 mRedrawScreen = true;
890 // Check the duration to determine if this was a short press
891 if (duration < ViewConfiguration.getLongPressTimeout()) {
892 switchViews(true /* trackball */);
894 mSelectionMode = SELECTION_LONGPRESS;
895 mRedrawScreen = true;
901 return super.onKeyUp(keyCode, event);
905 public boolean onKeyDown(int keyCode, KeyEvent event) {
906 if (mSelectionMode == SELECTION_HIDDEN) {
907 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
908 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
909 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
910 // Display the selection box but don't move or select it
911 // on this key press.
912 mSelectionMode = SELECTION_SELECTED;
913 mRedrawScreen = true;
916 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
917 // Display the selection box but don't select it
918 // on this key press.
919 mSelectionMode = SELECTION_PRESSED;
920 mRedrawScreen = true;
926 mSelectionMode = SELECTION_SELECTED;
929 int selectionDay = mSelectionDay;
932 case KeyEvent.KEYCODE_DEL:
933 // Delete the selected event, if any
934 Event selectedEvent = mSelectedEvent;
935 if (selectedEvent == null) {
940 long begin = selectedEvent.startMillis;
941 long end = selectedEvent.endMillis;
942 long id = selectedEvent.id;
943 mDeleteEventHelper.delete(begin, end, id, -1);
945 case KeyEvent.KEYCODE_ENTER:
946 switchViews(true /* trackball or keyboard */);
948 case KeyEvent.KEYCODE_BACK:
950 mParentActivity.finish();
952 case KeyEvent.KEYCODE_DPAD_LEFT:
953 if (mSelectedEvent != null) {
954 mSelectedEvent = mSelectedEvent.nextLeft;
956 if (mSelectedEvent == null) {
962 case KeyEvent.KEYCODE_DPAD_RIGHT:
963 if (mSelectedEvent != null) {
964 mSelectedEvent = mSelectedEvent.nextRight;
966 if (mSelectedEvent == null) {
972 case KeyEvent.KEYCODE_DPAD_UP:
973 if (mSelectedEvent != null) {
974 mSelectedEvent = mSelectedEvent.nextUp;
976 if (mSelectedEvent == null) {
977 if (!mSelectionAllDay) {
979 adjustHourSelection();
980 mSelectedEvents.clear();
981 mComputeSelectedEvents = true;
987 case KeyEvent.KEYCODE_DPAD_DOWN:
988 if (mSelectedEvent != null) {
989 mSelectedEvent = mSelectedEvent.nextDown;
991 if (mSelectedEvent == null) {
992 if (mSelectionAllDay) {
993 mSelectionAllDay = false;
996 adjustHourSelection();
997 mSelectedEvents.clear();
998 mComputeSelectedEvents = true;
1005 return super.onKeyDown(keyCode, event);
1008 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1010 CalendarView view = mParentActivity.getNextView();
1011 Time date = view.mBaseDate;
1012 date.set(mBaseDate);
1013 if (selectionDay < mFirstJulianDay) {
1014 date.monthDay -= mNumDays;
1017 date.monthDay += mNumDays;
1020 date.normalize(true /* ignore isDst */);
1021 view.mSelectionDay = selectionDay;
1024 mTitleTextView.setText(view.mDateRange);
1025 mParentActivity.switchViews(forward, 0, 0);
1028 mSelectionDay = selectionDay;
1029 mSelectedEvents.clear();
1030 mComputeSelectedEvents = true;
1033 mRedrawScreen = true;
1038 return super.onKeyDown(keyCode, event);
1041 // This is called after scrolling stops to move the selected hour
1042 // to the visible part of the screen.
1043 private void resetSelectedHour() {
1044 if (mSelectionHour < mFirstHour + 1) {
1045 mSelectionHour = mFirstHour + 1;
1046 mSelectedEvent = null;
1047 mSelectedEvents.clear();
1048 mComputeSelectedEvents = true;
1049 } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1050 mSelectionHour = mFirstHour + mNumHours - 3;
1051 mSelectedEvent = null;
1052 mSelectedEvents.clear();
1053 mComputeSelectedEvents = true;
1057 private void initFirstHour() {
1058 mFirstHour = mSelectionHour - mNumHours / 2;
1059 if (mFirstHour < 0) {
1061 } else if (mFirstHour + mNumHours > 24) {
1062 mFirstHour = 24 - mNumHours;
1067 * Recomputes the first full hour that is visible on screen after the
1068 * screen is scrolled.
1070 private void computeFirstHour() {
1071 // Compute the first full hour that is visible on screen
1072 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1073 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1076 private void adjustHourSelection() {
1077 if (mSelectionHour < 0) {
1079 if (mMaxAllDayEvents > 0) {
1080 mPrevSelectedEvent = null;
1081 mSelectionAllDay = true;
1085 if (mSelectionHour > 23) {
1086 mSelectionHour = 23;
1089 // If the selected hour is at least 2 time slots from the top and
1090 // bottom of the screen, then don't scroll the view.
1091 if (mSelectionHour < mFirstHour + 1) {
1092 // If there are all-days events for the selected day but there
1093 // are no more normal events earlier in the day, then jump to
1094 // the all-day event area.
1095 // Exception 1: allow the user to scroll to 8am with the trackball
1096 // before jumping to the all-day event area.
1097 // Exception 2: if 12am is on screen, then allow the user to select
1098 // 12am before going up to the all-day event area.
1099 int daynum = mSelectionDay - mFirstJulianDay;
1100 if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1101 && mFirstHour > 0 && mFirstHour < 8) {
1102 mPrevSelectedEvent = null;
1103 mSelectionAllDay = true;
1104 mSelectionHour = mFirstHour + 1;
1108 if (mFirstHour > 0) {
1110 mViewStartY -= (mCellHeight + HOUR_GAP);
1111 if (mViewStartY < 0) {
1118 if (mSelectionHour > mFirstHour + mNumHours - 3) {
1119 if (mFirstHour < 24 - mNumHours) {
1121 mViewStartY += (mCellHeight + HOUR_GAP);
1122 if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
1123 mViewStartY = mBitmapHeight - mGridAreaHeight;
1126 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1127 mViewStartY = mBitmapHeight - mGridAreaHeight;
1132 void clearCachedEvents() {
1133 mLastReloadMillis = 0;
1136 private Runnable mCancelCallback = new Runnable() {
1138 clearCachedEvents();
1142 void reloadEvents() {
1143 // Protect against this being called before this view has been
1145 if (mParentActivity == null) {
1149 mSelectedEvent = null;
1150 mPrevSelectedEvent = null;
1151 mSelectedEvents.clear();
1153 // The start date is the beginning of the week at 12am
1154 Time weekStart = new Time();
1155 weekStart.set(mBaseDate);
1157 weekStart.minute = 0;
1158 weekStart.second = 0;
1159 long millis = weekStart.normalize(true /* ignore isDst */);
1161 // Avoid reloading events unnecessarily.
1162 if (millis == mLastReloadMillis) {
1165 mLastReloadMillis = millis;
1167 // load events in the background
1168 mParentActivity.startProgressSpinner();
1169 final ArrayList<Event> events = new ArrayList<Event>();
1170 mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
1174 mRedrawScreen = true;
1175 mComputeSelectedEvents = true;
1177 mParentActivity.stopProgressSpinner();
1180 }, mCancelCallback);
1184 protected void onDraw(Canvas canvas) {
1186 remeasure(getWidth(), getHeight());
1190 if (mRedrawScreen && mCanvas != null) {
1192 mRedrawScreen = false;
1195 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1197 if (mViewStartX > 0) {
1198 canvas.translate(mViewWidth - mViewStartX, 0);
1200 canvas.translate(-(mViewWidth + mViewStartX), 0);
1202 CalendarView nextView = mParentActivity.getNextView();
1204 // Prevent infinite recursive calls to onDraw().
1205 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
1207 nextView.onDraw(canvas);
1210 canvas.translate(-mViewStartX, 0);
1213 if (mBitmap != null) {
1214 drawCalendarView(canvas);
1217 // Draw the fixed areas (that don't scroll) directly to the canvas.
1218 drawAfterScroll(canvas);
1219 mComputeSelectedEvents = false;
1221 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1226 private void drawCalendarView(Canvas canvas) {
1228 // Copy the scrollable region from the big bitmap to the canvas.
1229 Rect src = mSrcRect;
1230 Rect dest = mDestRect;
1232 src.top = mViewStartY;
1233 src.bottom = mViewStartY + mGridAreaHeight;
1235 src.right = mViewWidth;
1237 dest.top = mFirstCell;
1238 dest.bottom = mViewHeight;
1240 dest.right = mViewWidth;
1243 canvas.clipRect(dest);
1244 canvas.drawColor(0, PorterDuff.Mode.CLEAR);
1245 canvas.drawBitmap(mBitmap, src, dest, null);
1249 private void drawAfterScroll(Canvas canvas) {
1253 if (mMaxAllDayEvents != 0) {
1254 drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
1255 drawUpperLeftCorner(r, canvas, p);
1259 drawDayHeaderLoop(r, canvas, p);
1262 // Draw the AM and PM indicators if we're in 12 hour mode
1263 if (!mIs24HourFormat) {
1264 drawAmPm(canvas, p);
1267 // Update the popup window showing the event details, but only if
1268 // we are not scrolling and we have focus.
1269 if (!mScrolling && isFocused()) {
1270 updateEventDetails();
1274 // This isn't really the upper-left corner. It's the square area just
1275 // below the upper-left corner, above the hours and to the left of the
1277 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
1278 p.setColor(mCalendarHourBackground);
1279 r.top = mBannerPlusMargin;
1280 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1282 r.right = mHoursWidth;
1283 canvas.drawRect(r, p);
1286 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
1287 // Draw the horizontal day background banner
1288 p.setColor(mCalendarDateBannerBackground);
1290 r.bottom = mBannerPlusMargin;
1292 r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
1293 canvas.drawRect(r, p);
1295 // Fill the extra space on the right side with the default background
1297 r.right = mViewWidth;
1298 p.setColor(mCalendarGridAreaBackground);
1299 canvas.drawRect(r, p);
1301 // Draw a highlight on the selected day (if any), but only if we are
1302 // displaying more than one day.
1303 if (mSelectionMode != SELECTION_HIDDEN) {
1305 p.setColor(mCalendarDateSelected);
1307 r.bottom = mBannerPlusMargin;
1308 int daynum = mSelectionDay - mFirstJulianDay;
1309 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1310 r.right = r.left + mCellWidth;
1311 canvas.drawRect(r, p);
1315 p.setTextSize(NORMAL_FONT_SIZE);
1316 p.setTextAlign(Paint.Align.CENTER);
1317 int x = mHoursWidth;
1318 int deltaX = mCellWidth + DAY_GAP;
1319 int cell = mFirstJulianDay;
1322 if (mDateStrWidth < mCellWidth) {
1323 dayNames = mDayStrs;
1325 dayNames = mDayStrs2Letter;
1328 p.setTypeface(mBold);
1329 p.setAntiAlias(true);
1330 for (int day = 0; day < mNumDays; day++, cell++) {
1331 drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
1336 private void drawAmPm(Canvas canvas, Paint p) {
1337 p.setColor(mCalendarAmPmLabel);
1338 p.setTextSize(AMPM_FONT_SIZE);
1339 p.setTypeface(mBold);
1340 p.setAntiAlias(true);
1341 mPaint.setTextAlign(Paint.Align.RIGHT);
1342 String text = mAmString;
1343 if (mFirstHour >= 12) {
1346 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
1347 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1348 canvas.drawText(text, right, y, p);
1350 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
1351 // Also draw the "PM"
1353 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
1354 + 2 * mHoursTextHeight + HOUR_GAP;
1355 canvas.drawText(text, right, y, p);
1359 private void doDraw(Canvas canvas) {
1363 drawGridBackground(r, canvas, p);
1364 drawHours(r, canvas, p);
1367 int x = mHoursWidth;
1368 int deltaX = mCellWidth + DAY_GAP;
1369 int cell = mFirstJulianDay;
1370 for (int day = 0; day < mNumDays; day++, cell++) {
1371 drawEvents(cell, x, HOUR_GAP, canvas, p);
1376 private void drawHours(Rect r, Canvas canvas, Paint p) {
1377 // Draw the background for the hour labels
1378 p.setColor(mCalendarHourBackground);
1380 r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
1382 r.right = mHoursWidth;
1383 canvas.drawRect(r, p);
1385 // Fill the bottom left corner with the default grid background
1387 r.bottom = mBitmapHeight;
1388 p.setColor(mCalendarGridAreaBackground);
1389 canvas.drawRect(r, p);
1391 // Draw a highlight on the selected hour (if needed)
1392 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
1393 p.setColor(mCalendarHourSelected);
1394 r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1395 r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
1397 r.right = mHoursWidth;
1398 canvas.drawRect(r, p);
1400 // Also draw the highlight on the grid
1401 p.setColor(mCalendarGridAreaSelected);
1402 int daynum = mSelectionDay - mFirstJulianDay;
1403 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1404 r.right = r.left + mCellWidth;
1405 canvas.drawRect(r, p);
1407 // Draw a border around the highlighted grid hour.
1410 r.bottom -= HOUR_GAP;
1412 path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
1413 canvas.drawPath(path, mSelectionPaint);
1414 saveSelectionPosition(r.left, r.top, r.right, r.bottom);
1417 p.setColor(mCalendarHourLabel);
1418 p.setTextSize(HOURS_FONT_SIZE);
1419 p.setTypeface(mBold);
1420 p.setTextAlign(Paint.Align.RIGHT);
1421 p.setAntiAlias(true);
1423 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1424 int y = HOUR_GAP + mHoursTextHeight;
1426 for (int i = 0; i < 24; i++) {
1427 String time = mHourStrs[i];
1428 canvas.drawText(time, right, y, p);
1429 y += mCellHeight + HOUR_GAP;
1433 private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
1434 float xCenter = x + mCellWidth / 2.0f;
1436 boolean isWeekend = false;
1437 if ((mStartDay == Time.SUNDAY && (day == 0 || day == 6))
1438 || (mStartDay == Time.MONDAY && (day == 5 || day == 6))
1439 || (mStartDay == Time.SATURDAY && (day == 0 || day == 1))) {
1444 p.setColor(mWeek_weekendColor);
1446 p.setColor(mCalendarDateBannerTextColor);
1449 int dateNum = mFirstDate + day;
1450 if (dateNum > mMonthLength) {
1451 dateNum -= mMonthLength;
1455 // Add a leading zero if the date is a single digit
1457 dateNumStr = "0" + dateNum;
1459 dateNumStr = String.valueOf(dateNum);
1462 DayHeader header = dayHeaders[day];
1463 if (header == null || header.cell != cell) {
1464 // The day header string is regenerated on every draw during drag and fling animation.
1465 // Caching day header since formatting the string takes surprising long time.
1467 dayHeaders[day] = new DayHeader();
1468 dayHeaders[day].cell = cell;
1469 dayHeaders[day].dateString = getResources().getString(
1470 R.string.weekday_day, dateStr, dateNumStr);
1472 dateStr = dayHeaders[day].dateString;
1474 float y = mBannerPlusMargin - 7;
1475 canvas.drawText(dateStr, xCenter, y, p);
1478 private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
1479 Paint.Style savedStyle = p.getStyle();
1481 // Clear the background
1482 p.setColor(mCalendarGridAreaBackground);
1484 r.bottom = mBitmapHeight;
1486 r.right = mViewWidth;
1487 canvas.drawRect(r, p);
1489 // Draw the horizontal grid lines
1490 p.setColor(mCalendarGridLineHorizontalColor);
1491 p.setStyle(Style.STROKE);
1492 p.setStrokeWidth(0);
1493 p.setAntiAlias(false);
1494 float startX = mHoursWidth;
1495 float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
1497 float deltaY = mCellHeight + HOUR_GAP;
1498 for (int hour = 0; hour <= 24; hour++) {
1499 canvas.drawLine(startX, y, stopX, y, p);
1503 // Draw the vertical grid lines
1504 p.setColor(mCalendarGridLineVerticalColor);
1506 float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
1507 float deltaX = mCellWidth + DAY_GAP;
1508 float x = mHoursWidth + mCellWidth;
1509 for (int day = 0; day < mNumDays; day++) {
1510 canvas.drawLine(x, startY, x, stopY, p);
1514 // Restore the saved style.
1515 p.setStyle(savedStyle);
1516 p.setAntiAlias(true);
1519 Event getSelectedEvent() {
1520 if (mSelectedEvent == null) {
1521 // There is no event at the selected hour, so create a new event.
1522 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1523 getSelectedMinutesSinceMidnight());
1525 return mSelectedEvent;
1528 boolean isEventSelected() {
1529 return (mSelectedEvent != null);
1532 Event getNewEvent() {
1533 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1534 getSelectedMinutesSinceMidnight());
1537 static Event getNewEvent(int julianDay, long utcMillis,
1538 int minutesSinceMidnight) {
1539 Event event = Event.newInstance();
1540 event.startDay = julianDay;
1541 event.endDay = julianDay;
1542 event.startMillis = utcMillis;
1543 event.endMillis = event.startMillis + MILLIS_PER_HOUR;
1544 event.startTime = minutesSinceMidnight;
1545 event.endTime = event.startTime + MINUTES_PER_HOUR;
1549 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
1550 float maxWidthF = 0.0f;
1552 int len = strings.length;
1553 for (int i = 0; i < len; i++) {
1554 float width = p.measureText(strings[i]);
1555 maxWidthF = Math.max(width, maxWidthF);
1557 int maxWidth = (int) (maxWidthF + 0.5);
1558 if (maxWidth < currentMax) {
1559 maxWidth = currentMax;
1564 private void saveSelectionPosition(float left, float top, float right, float bottom) {
1565 mPrevBox.left = (int) left;
1566 mPrevBox.right = (int) right;
1567 mPrevBox.top = (int) top;
1568 mPrevBox.bottom = (int) bottom;
1571 private Rect getCurrentSelectionPosition() {
1572 Rect box = new Rect();
1573 box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1574 box.bottom = box.top + mCellHeight + HOUR_GAP;
1575 int daynum = mSelectionDay - mFirstJulianDay;
1576 box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1577 box.right = box.left + mCellWidth + DAY_GAP;
1581 private void drawAllDayEvents(int firstDay, int numDays,
1582 Rect r, Canvas canvas, Paint p) {
1583 p.setTextSize(NORMAL_FONT_SIZE);
1584 p.setTextAlign(Paint.Align.LEFT);
1585 Paint eventTextPaint = mEventTextPaint;
1587 // Draw the background for the all-day events area
1588 r.top = mBannerPlusMargin;
1589 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1590 r.left = mHoursWidth;
1591 r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
1592 p.setColor(mCalendarAllDayBackground);
1593 canvas.drawRect(r, p);
1595 // Fill the extra space on the right side with the default background
1597 r.right = mViewWidth;
1598 p.setColor(mCalendarGridAreaBackground);
1599 canvas.drawRect(r, p);
1601 // Draw the vertical grid lines
1602 p.setColor(mCalendarGridLineVerticalColor);
1603 p.setStyle(Style.STROKE);
1604 p.setStrokeWidth(0);
1605 p.setAntiAlias(false);
1606 float startY = r.top;
1607 float stopY = r.bottom;
1608 float deltaX = mCellWidth + DAY_GAP;
1609 float x = mHoursWidth + mCellWidth;
1610 for (int day = 0; day <= mNumDays; day++) {
1611 canvas.drawLine(x, startY, x, stopY, p);
1614 p.setAntiAlias(true);
1615 p.setStyle(Style.FILL);
1617 int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
1618 float left = mHoursWidth;
1619 int lastDay = firstDay + numDays - 1;
1620 ArrayList<Event> events = mEvents;
1621 int numEvents = events.size();
1622 float drawHeight = mAllDayHeight;
1623 float numRectangles = mMaxAllDayEvents;
1624 for (int i = 0; i < numEvents; i++) {
1625 Event event = events.get(i);
1628 int startDay = event.startDay;
1629 int endDay = event.endDay;
1630 if (startDay > lastDay || endDay < firstDay)
1632 if (startDay < firstDay)
1633 startDay = firstDay;
1634 if (endDay > lastDay)
1636 int startIndex = startDay - firstDay;
1637 int endIndex = endDay - firstDay;
1638 float height = drawHeight / numRectangles;
1640 // Prevent a single event from getting too big
1641 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
1642 height = MAX_ALLDAY_EVENT_HEIGHT;
1645 // Leave a one-pixel space between the vertical day lines and the
1647 event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
1648 event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
1649 event.top = y + height * event.getColumn();
1651 // Multiply the height by 0.9 to leave a little gap between events
1652 event.bottom = event.top + height * 0.9f;
1654 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1655 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1657 // Check if this all-day event intersects the selected day
1658 if (mSelectionAllDay && mComputeSelectedEvents) {
1659 if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
1660 mSelectedEvents.add(event);
1665 if (mSelectionAllDay) {
1666 // Compute the neighbors for the list of all-day events that
1667 // intersect the selected day.
1668 computeAllDayNeighbors();
1669 if (mSelectedEvent != null) {
1670 Event event = mSelectedEvent;
1671 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1672 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1675 // Draw the highlight on the selected all-day area
1676 float top = mBannerPlusMargin + 1;
1677 float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
1678 int daynum = mSelectionDay - mFirstJulianDay;
1679 left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
1680 float right = left + mCellWidth + DAY_GAP - 1;
1681 if (mNumDays == 1) {
1682 // The Day view doesn't have a vertical line on the right.
1687 path.addRect(left, top, right, bottom, Direction.CW);
1688 canvas.drawPath(path, mSelectionPaint);
1690 // Set the selection position to zero so that when we move down
1691 // to the normal event area, we will highlight the topmost event.
1692 saveSelectionPosition(0f, 0f, 0f, 0f);
1696 private void computeAllDayNeighbors() {
1697 int len = mSelectedEvents.size();
1698 if (len == 0 || mSelectedEvent != null) {
1702 // First, clear all the links
1703 for (int ii = 0; ii < len; ii++) {
1704 Event ev = mSelectedEvents.get(ii);
1708 ev.nextRight = null;
1711 // For each event in the selected event list "mSelectedEvents", find
1712 // its neighbors in the up and down directions. This could be done
1713 // more efficiently by sorting on the Event.getColumn() field, but
1714 // the list is expected to be very small.
1716 // Find the event in the same row as the previously selected all-day
1718 int startPosition = -1;
1719 if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
1720 startPosition = mPrevSelectedEvent.getColumn();
1722 int maxPosition = -1;
1723 Event startEvent = null;
1724 Event maxPositionEvent = null;
1725 for (int ii = 0; ii < len; ii++) {
1726 Event ev = mSelectedEvents.get(ii);
1727 int position = ev.getColumn();
1728 if (position == startPosition) {
1730 } else if (position > maxPosition) {
1731 maxPositionEvent = ev;
1732 maxPosition = position;
1734 for (int jj = 0; jj < len; jj++) {
1738 Event neighbor = mSelectedEvents.get(jj);
1739 int neighborPosition = neighbor.getColumn();
1740 if (neighborPosition == position - 1) {
1741 ev.nextUp = neighbor;
1742 } else if (neighborPosition == position + 1) {
1743 ev.nextDown = neighbor;
1747 if (startEvent != null) {
1748 mSelectedEvent = startEvent;
1750 mSelectedEvent = maxPositionEvent;
1754 RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
1755 // If this event is selected, then use the selection color
1756 if (mSelectedEvent == event) {
1757 // Also, remember the last selected event that we drew
1758 mPrevSelectedEvent = event;
1759 p.setColor(mSelectionColor);
1760 eventTextPaint.setColor(mSelectedEventTextColor);
1762 // Use the normal color for all-day events
1763 p.setColor(event.color);
1764 eventTextPaint.setColor(mEventTextColor);
1769 rf.bottom = event.bottom;
1770 rf.left = event.left;
1771 rf.right = event.right;
1772 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
1779 private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
1780 Paint eventTextPaint = mEventTextPaint;
1781 int cellWidth = mCellWidth;
1782 int cellHeight = mCellHeight;
1784 // Use the selected hour as the selection region
1785 Rect selectionArea = mRect;
1786 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
1787 selectionArea.bottom = selectionArea.top + cellHeight;
1788 selectionArea.left = left;
1789 selectionArea.right = selectionArea.left + cellWidth;
1791 ArrayList<Event> events = mEvents;
1792 int numEvents = events.size();
1793 EventGeometry geometry = mEventGeometry;
1795 for (int i = 0; i < numEvents; i++) {
1796 Event event = events.get(i);
1797 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
1801 if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
1802 && geometry.eventIntersectsSelection(event, selectionArea)) {
1803 mSelectedEvents.add(event);
1806 RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
1807 drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1810 if (date == mSelectionDay && !mSelectionAllDay && isFocused()
1811 && mSelectionMode != SELECTION_HIDDEN) {
1813 if (mSelectedEvent != null) {
1814 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
1815 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1820 // Computes the "nearest" neighbor event in four directions (left, right,
1821 // up, down) for each of the events in the mSelectedEvents array.
1822 private void computeNeighbors() {
1823 int len = mSelectedEvents.size();
1824 if (len == 0 || mSelectedEvent != null) {
1828 // First, clear all the links
1829 for (int ii = 0; ii < len; ii++) {
1830 Event ev = mSelectedEvents.get(ii);
1834 ev.nextRight = null;
1837 Event startEvent = mSelectedEvents.get(0);
1838 int startEventDistance1 = 100000; // any large number
1839 int startEventDistance2 = 100000; // any large number
1840 int prevLocation = FROM_NONE;
1846 Rect box = getCurrentSelectionPosition();
1847 if (mPrevSelectedEvent != null) {
1848 prevTop = (int) mPrevSelectedEvent.top;
1849 prevBottom = (int) mPrevSelectedEvent.bottom;
1850 prevLeft = (int) mPrevSelectedEvent.left;
1851 prevRight = (int) mPrevSelectedEvent.right;
1852 // Check if the previously selected event intersects the previous
1853 // selection box. (The previously selected event may be from a
1854 // much older selection box.)
1855 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
1856 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
1857 mPrevSelectedEvent = null;
1858 prevTop = mPrevBox.top;
1859 prevBottom = mPrevBox.bottom;
1860 prevLeft = mPrevBox.left;
1861 prevRight = mPrevBox.right;
1863 // Clip the top and bottom to the previous selection box.
1864 if (prevTop < mPrevBox.top) {
1865 prevTop = mPrevBox.top;
1867 if (prevBottom > mPrevBox.bottom) {
1868 prevBottom = mPrevBox.bottom;
1872 // Just use the previously drawn selection box
1873 prevTop = mPrevBox.top;
1874 prevBottom = mPrevBox.bottom;
1875 prevLeft = mPrevBox.left;
1876 prevRight = mPrevBox.right;
1879 // Figure out where we came from and compute the center of that area.
1880 if (prevLeft >= box.right) {
1881 // The previously selected event was to the right of us.
1882 prevLocation = FROM_RIGHT;
1883 prevCenter = (prevTop + prevBottom) / 2;
1884 } else if (prevRight <= box.left) {
1885 // The previously selected event was to the left of us.
1886 prevLocation = FROM_LEFT;
1887 prevCenter = (prevTop + prevBottom) / 2;
1888 } else if (prevBottom <= box.top) {
1889 // The previously selected event was above us.
1890 prevLocation = FROM_ABOVE;
1891 prevCenter = (prevLeft + prevRight) / 2;
1892 } else if (prevTop >= box.bottom) {
1893 // The previously selected event was below us.
1894 prevLocation = FROM_BELOW;
1895 prevCenter = (prevLeft + prevRight) / 2;
1898 // For each event in the selected event list "mSelectedEvents", search
1899 // all the other events in that list for the nearest neighbor in 4
1901 for (int ii = 0; ii < len; ii++) {
1902 Event ev = mSelectedEvents.get(ii);
1904 int startTime = ev.startTime;
1905 int endTime = ev.endTime;
1906 int left = (int) ev.left;
1907 int right = (int) ev.right;
1908 int top = (int) ev.top;
1909 if (top < box.top) {
1912 int bottom = (int) ev.bottom;
1913 if (bottom > box.bottom) {
1914 bottom = box.bottom;
1917 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
1918 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1919 if (DateFormat.is24HourFormat(mContext)) {
1920 flags |= DateUtils.FORMAT_24HOUR;
1922 String timeRange = DateUtils.formatDateRange(mParentActivity,
1923 ev.startMillis, ev.endMillis, flags);
1924 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
1925 + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
1927 int upDistanceMin = 10000; // any large number
1928 int downDistanceMin = 10000; // any large number
1929 int leftDistanceMin = 10000; // any large number
1930 int rightDistanceMin = 10000; // any large number
1931 Event upEvent = null;
1932 Event downEvent = null;
1933 Event leftEvent = null;
1934 Event rightEvent = null;
1936 // Pick the starting event closest to the previously selected event,
1937 // if any. distance1 takes precedence over distance2.
1940 if (prevLocation == FROM_ABOVE) {
1941 if (left >= prevCenter) {
1942 distance1 = left - prevCenter;
1943 } else if (right <= prevCenter) {
1944 distance1 = prevCenter - right;
1946 distance2 = top - prevBottom;
1947 } else if (prevLocation == FROM_BELOW) {
1948 if (left >= prevCenter) {
1949 distance1 = left - prevCenter;
1950 } else if (right <= prevCenter) {
1951 distance1 = prevCenter - right;
1953 distance2 = prevTop - bottom;
1954 } else if (prevLocation == FROM_LEFT) {
1955 if (bottom <= prevCenter) {
1956 distance1 = prevCenter - bottom;
1957 } else if (top >= prevCenter) {
1958 distance1 = top - prevCenter;
1960 distance2 = left - prevRight;
1961 } else if (prevLocation == FROM_RIGHT) {
1962 if (bottom <= prevCenter) {
1963 distance1 = prevCenter - bottom;
1964 } else if (top >= prevCenter) {
1965 distance1 = top - prevCenter;
1967 distance2 = prevLeft - right;
1969 if (distance1 < startEventDistance1
1970 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
1972 startEventDistance1 = distance1;
1973 startEventDistance2 = distance2;
1976 // For each neighbor, figure out if it is above or below or left
1977 // or right of me and compute the distance.
1978 for (int jj = 0; jj < len; jj++) {
1982 Event neighbor = mSelectedEvents.get(jj);
1983 int neighborLeft = (int) neighbor.left;
1984 int neighborRight = (int) neighbor.right;
1985 if (neighbor.endTime <= startTime) {
1986 // This neighbor is entirely above me.
1987 // If we overlap the same column, then compute the distance.
1988 if (neighborLeft < right && neighborRight > left) {
1989 int distance = startTime - neighbor.endTime;
1990 if (distance < upDistanceMin) {
1991 upDistanceMin = distance;
1993 } else if (distance == upDistanceMin) {
1994 int center = (left + right) / 2;
1995 int currentDistance = 0;
1996 int currentLeft = (int) upEvent.left;
1997 int currentRight = (int) upEvent.right;
1998 if (currentRight <= center) {
1999 currentDistance = center - currentRight;
2000 } else if (currentLeft >= center) {
2001 currentDistance = currentLeft - center;
2004 int neighborDistance = 0;
2005 if (neighborRight <= center) {
2006 neighborDistance = center - neighborRight;
2007 } else if (neighborLeft >= center) {
2008 neighborDistance = neighborLeft - center;
2010 if (neighborDistance < currentDistance) {
2011 upDistanceMin = distance;
2016 } else if (neighbor.startTime >= endTime) {
2017 // This neighbor is entirely below me.
2018 // If we overlap the same column, then compute the distance.
2019 if (neighborLeft < right && neighborRight > left) {
2020 int distance = neighbor.startTime - endTime;
2021 if (distance < downDistanceMin) {
2022 downDistanceMin = distance;
2023 downEvent = neighbor;
2024 } else if (distance == downDistanceMin) {
2025 int center = (left + right) / 2;
2026 int currentDistance = 0;
2027 int currentLeft = (int) downEvent.left;
2028 int currentRight = (int) downEvent.right;
2029 if (currentRight <= center) {
2030 currentDistance = center - currentRight;
2031 } else if (currentLeft >= center) {
2032 currentDistance = currentLeft - center;
2035 int neighborDistance = 0;
2036 if (neighborRight <= center) {
2037 neighborDistance = center - neighborRight;
2038 } else if (neighborLeft >= center) {
2039 neighborDistance = neighborLeft - center;
2041 if (neighborDistance < currentDistance) {
2042 downDistanceMin = distance;
2043 downEvent = neighbor;
2049 if (neighborLeft >= right) {
2050 // This neighbor is entirely to the right of me.
2051 // Take the closest neighbor in the y direction.
2052 int center = (top + bottom) / 2;
2054 int neighborBottom = (int) neighbor.bottom;
2055 int neighborTop = (int) neighbor.top;
2056 if (neighborBottom <= center) {
2057 distance = center - neighborBottom;
2058 } else if (neighborTop >= center) {
2059 distance = neighborTop - center;
2061 if (distance < rightDistanceMin) {
2062 rightDistanceMin = distance;
2063 rightEvent = neighbor;
2064 } else if (distance == rightDistanceMin) {
2065 // Pick the closest in the x direction
2066 int neighborDistance = neighborLeft - right;
2067 int currentDistance = (int) rightEvent.left - right;
2068 if (neighborDistance < currentDistance) {
2069 rightDistanceMin = distance;
2070 rightEvent = neighbor;
2073 } else if (neighborRight <= left) {
2074 // This neighbor is entirely to the left of me.
2075 // Take the closest neighbor in the y direction.
2076 int center = (top + bottom) / 2;
2078 int neighborBottom = (int) neighbor.bottom;
2079 int neighborTop = (int) neighbor.top;
2080 if (neighborBottom <= center) {
2081 distance = center - neighborBottom;
2082 } else if (neighborTop >= center) {
2083 distance = neighborTop - center;
2085 if (distance < leftDistanceMin) {
2086 leftDistanceMin = distance;
2087 leftEvent = neighbor;
2088 } else if (distance == leftDistanceMin) {
2089 // Pick the closest in the x direction
2090 int neighborDistance = left - neighborRight;
2091 int currentDistance = left - (int) leftEvent.right;
2092 if (neighborDistance < currentDistance) {
2093 leftDistanceMin = distance;
2094 leftEvent = neighbor;
2099 ev.nextUp = upEvent;
2100 ev.nextDown = downEvent;
2101 ev.nextLeft = leftEvent;
2102 ev.nextRight = rightEvent;
2104 mSelectedEvent = startEvent;
2108 private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
2110 int color = event.color;
2112 // Fade visible boxes if event was declined.
2113 boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
2115 int alpha = color & 0xff000000;
2116 color &= 0x00ffffff;
2117 int red = (color & 0x00ff0000) >> 16;
2118 int green = (color & 0x0000ff00) >> 8;
2119 int blue = (color & 0x0000ff);
2120 color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
2121 color += 0x7F7F7F + alpha;
2124 // If this event is selected, then use the selection color
2125 if (mSelectedEvent == event) {
2126 if (mSelectionMode == SELECTION_PRESSED) {
2127 // Also, remember the last selected event that we drew
2128 mPrevSelectedEvent = event;
2129 // box = mBoxPressed;
2130 p.setColor(mPressedColor); // FIXME:pressed
2131 eventTextPaint.setColor(mSelectedEventTextColor);
2132 } else if (mSelectionMode == SELECTION_SELECTED) {
2133 // Also, remember the last selected event that we drew
2134 mPrevSelectedEvent = event;
2135 // box = mBoxSelected;
2136 p.setColor(mSelectionColor);
2137 eventTextPaint.setColor(mSelectedEventTextColor);
2138 } else if (mSelectionMode == SELECTION_LONGPRESS) {
2139 // box = mBoxLongPressed;
2140 p.setColor(mPressedColor); // FIXME: longpressed (maybe -- this doesn't seem to work)
2141 eventTextPaint.setColor(mSelectedEventTextColor);
2144 eventTextPaint.setColor(mEventTextColor);
2148 eventTextPaint.setColor(mEventTextColor);
2154 rf.bottom = event.bottom;
2155 rf.left = event.left;
2156 rf.right = event.right - 1;
2158 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
2160 // Draw a darker border
2161 float[] hsv = new float[3];
2162 Color.colorToHSV(p.getColor(), hsv);
2165 mPaintBorder.setColor(Color.HSVToColor(hsv));
2166 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
2174 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
2176 // Sanitize a string before passing it to drawText or else we get little
2177 // squares. For newlines and tabs before a comma, delete the character.
2178 // Otherwise, just replace them with a space.
2179 private String drawTextSanitizer(String string) {
2180 Matcher m = drawTextSanitizerFilter.matcher(string);
2181 string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
2185 private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
2186 if (!mDrawTextInEventRect) {
2190 float width = rf.right - rf.left;
2191 float height = rf.bottom - rf.top;
2193 // Leave one pixel extra space between lines
2194 int lineHeight = mEventTextHeight + 1;
2196 // If the rectangle is too small for text, then return
2197 if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
2201 // Truncate the event title to a known (large enough) limit
2202 String text = event.getTitleAndLocation();
2204 text = drawTextSanitizer(text);
2206 int len = text.length();
2207 if (len > MAX_EVENT_TEXT_LEN) {
2208 text = text.substring(0, MAX_EVENT_TEXT_LEN);
2209 len = MAX_EVENT_TEXT_LEN;
2212 // Figure out how much space the event title will take, and create a
2213 // String fragment that will fit in the rectangle. Use multiple lines,
2215 p.getTextWidths(text, mCharWidths);
2216 String fragment = text;
2217 float top = rf.top + mEventTextAscent + topMargin;
2220 // Leave one pixel extra space at the bottom
2221 while (start < len && height >= (lineHeight + 1)) {
2222 boolean lastLine = (height < 2 * lineHeight + 1);
2223 // Skip leading spaces at the beginning of each line
2225 char c = text.charAt(start);
2226 if (c != ' ') break;
2228 } while (start < len);
2232 for (int ii = start; ii < len; ii++) {
2233 char c = text.charAt(ii);
2235 // If we found the end of a word, then remember the ending
2240 sum += mCharWidths[ii];
2241 // If adding this character would exceed the width and this
2242 // isn't the last line, then break the line at the previous
2243 // word. If there was no previous word, then break this word.
2245 if (end > start && !lastLine) {
2246 // There was a previous word on this line.
2247 fragment = text.substring(start, end);
2252 // This is the only word and it is too long to fit on
2253 // the line (or this is the last line), so take as many
2254 // characters of this word as will fit.
2255 fragment = text.substring(start, ii);
2261 // If sum <= width, then we can fit the rest of the text on
2264 fragment = text.substring(start, len);
2268 canvas.drawText(fragment, rf.left + 1, top, p);
2271 height -= lineHeight;
2275 private void updateEventDetails() {
2276 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
2277 || mSelectionMode == SELECTION_LONGPRESS) {
2282 // Remove any outstanding callbacks to dismiss the popup.
2283 getHandler().removeCallbacks(mDismissPopup);
2285 Event event = mSelectedEvent;
2286 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
2287 titleView.setText(event.title);
2289 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
2290 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
2292 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
2293 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
2297 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
2298 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
2300 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
2301 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
2302 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2304 if (DateFormat.is24HourFormat(mContext)) {
2305 flags |= DateUtils.FORMAT_24HOUR;
2307 String timeRange = DateUtils.formatDateRange(mParentActivity,
2308 event.startMillis, event.endMillis, flags);
2309 TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
2310 timeView.setText(timeRange);
2312 TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
2313 final boolean empty = TextUtils.isEmpty(event.location);
2314 whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
2315 if (!empty) whereView.setText(event.location);
2317 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
2318 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
2321 // The following routines are called from the parent activity when certain
2322 // touch events occur.
2324 void doDown(MotionEvent ev) {
2325 mTouchMode = TOUCH_MODE_DOWN;
2327 mOnFlingCalled = false;
2328 mLaunchNewView = false;
2329 getHandler().removeCallbacks(mContinueScroll);
2332 void doSingleTapUp(MotionEvent ev) {
2333 mSelectionMode = SELECTION_SELECTED;
2334 mRedrawScreen = true;
2336 if (mLaunchNewView) {
2337 mLaunchNewView = false;
2338 switchViews(false /* not the trackball */);
2342 void doShowPress(MotionEvent ev) {
2343 int x = (int) ev.getX();
2344 int y = (int) ev.getY();
2345 Event selectedEvent = mSelectedEvent;
2346 int selectedDay = mSelectionDay;
2347 int selectedHour = mSelectionHour;
2349 boolean validPosition = setSelectionFromPosition(x, y);
2350 if (!validPosition) {
2354 mSelectionMode = SELECTION_PRESSED;
2355 mRedrawScreen = true;
2358 // If the tap is on an already selected event or hour slot,
2359 // then launch a new view. Otherwise, just select the event.
2360 if (selectedEvent != null && selectedEvent == mSelectedEvent) {
2361 // Launch the "View event" view when the finger lifts up,
2362 // unless the finger moves before lifting up.
2363 mLaunchNewView = true;
2364 } else if (selectedEvent == null && selectedDay == mSelectionDay
2365 && selectedHour == mSelectionHour) {
2366 // Launch the Day/Agenda view when the finger lifts up,
2367 // unless the finger moves before lifting up.
2368 mLaunchNewView = true;
2372 void doLongPress(MotionEvent ev) {
2373 mLaunchNewView = false;
2374 mSelectionMode = SELECTION_LONGPRESS;
2375 mRedrawScreen = true;
2380 void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
2381 mLaunchNewView = false;
2382 // Use the distance from the current point to the initial touch instead
2383 // of deltaX and deltaY to avoid accumulating floating-point rounding
2384 // errors. Also, we don't need floats, we can use ints.
2385 int distanceX = (int) e1.getX() - (int) e2.getX();
2386 int distanceY = (int) e1.getY() - (int) e2.getY();
2388 // If we haven't figured out the predominant scroll direction yet,
2390 if (mTouchMode == TOUCH_MODE_DOWN) {
2391 int absDistanceX = Math.abs(distanceX);
2392 int absDistanceY = Math.abs(distanceY);
2393 mScrollStartY = mViewStartY;
2394 mPreviousDistanceX = 0;
2395 mPreviousDirection = 0;
2397 // If the x distance is at least twice the y distance, then lock
2398 // the scroll horizontally. Otherwise scroll vertically.
2399 if (absDistanceX >= 2 * absDistanceY) {
2400 mTouchMode = TOUCH_MODE_HSCROLL;
2401 mViewStartX = distanceX;
2402 initNextView(-mViewStartX);
2404 mTouchMode = TOUCH_MODE_VSCROLL;
2406 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2407 // We are already scrolling horizontally, so check if we
2408 // changed the direction of scrolling so that the other week
2410 mViewStartX = distanceX;
2411 if (distanceX != 0) {
2412 int direction = (distanceX > 0) ? 1 : -1;
2413 if (direction != mPreviousDirection) {
2414 // The user has switched the direction of scrolling
2415 // so re-init the next view
2416 initNextView(-mViewStartX);
2417 mPreviousDirection = direction;
2421 // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
2422 // then change the title to the new day (or week), but only
2423 // if we haven't already changed the title.
2424 if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
2425 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
2426 CalendarView view = mParentActivity.getNextView();
2427 mTitleTextView.setText(view.mDateRange);
2429 } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2430 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
2431 CalendarView view = mParentActivity.getNextView();
2432 mTitleTextView.setText(view.mDateRange);
2435 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
2436 || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2437 mTitleTextView.setText(mDateRange);
2440 mPreviousDistanceX = distanceX;
2443 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
2444 mViewStartY = mScrollStartY + distanceY;
2445 if (mViewStartY < 0) {
2447 } else if (mViewStartY > mMaxViewStartY) {
2448 mViewStartY = mMaxViewStartY;
2455 if (mSelectionMode != SELECTION_HIDDEN) {
2456 mSelectionMode = SELECTION_HIDDEN;
2457 mRedrawScreen = true;
2462 void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2463 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2464 mSelectionMode = SELECTION_HIDDEN;
2465 mOnFlingCalled = true;
2466 int deltaX = (int) e2.getX() - (int) e1.getX();
2467 int distanceX = Math.abs(deltaX);
2468 int deltaY = (int) e2.getY() - (int) e1.getY();
2469 int distanceY = Math.abs(deltaY);
2471 if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
2472 boolean switchForward = initNextView(deltaX);
2473 CalendarView view = mParentActivity.getNextView();
2474 mTitleTextView.setText(view.mDateRange);
2475 mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
2480 // Continue scrolling vertically
2481 mContinueScroll.init((int) velocityY / 20);
2482 post(mContinueScroll);
2485 private boolean initNextView(int deltaX) {
2486 // Change the view to the previous day or week
2487 CalendarView view = mParentActivity.getNextView();
2488 Time date = view.mBaseDate;
2489 date.set(mBaseDate);
2490 boolean switchForward;
2492 date.monthDay -= mNumDays;
2493 view.mSelectionDay = mSelectionDay - mNumDays;
2494 switchForward = false;
2496 date.monthDay += mNumDays;
2497 view.mSelectionDay = mSelectionDay + mNumDays;
2498 switchForward = true;
2500 date.normalize(true /* ignore isDst */);
2502 view.setFrame(getLeft(), getTop(), getRight(), getBottom());
2503 view.reloadEvents();
2504 return switchForward;
2508 public boolean onTouchEvent(MotionEvent ev) {
2509 int action = ev.getAction();
2512 case MotionEvent.ACTION_DOWN:
2513 mParentActivity.mGestureDetector.onTouchEvent(ev);
2516 case MotionEvent.ACTION_MOVE:
2517 mParentActivity.mGestureDetector.onTouchEvent(ev);
2520 case MotionEvent.ACTION_UP:
2521 mParentActivity.mGestureDetector.onTouchEvent(ev);
2522 if (mOnFlingCalled) {
2525 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2526 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2527 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
2528 // The user has gone beyond the threshold so switch views
2529 mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
2533 // Not beyond the threshold so invalidate which will cause
2534 // the view to snap back. Also call recalc() to ensure
2535 // that we have the correct starting date and title.
2537 mTitleTextView.setText(mDateRange);
2543 // If we were scrolling, then reset the selected hour so that it
2547 resetSelectedHour();
2548 mRedrawScreen = true;
2553 // This case isn't expected to happen.
2554 case MotionEvent.ACTION_CANCEL:
2555 mParentActivity.mGestureDetector.onTouchEvent(ev);
2557 resetSelectedHour();
2561 if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
2564 return super.onTouchEvent(ev);
2568 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
2571 // If the trackball is held down, then the context menu pops up and
2572 // we never get onKeyUp() for the long-press. So check for it here
2573 // and change the selection to the long-press state.
2574 if (mSelectionMode != SELECTION_LONGPRESS) {
2575 mSelectionMode = SELECTION_LONGPRESS;
2576 mRedrawScreen = true;
2580 final long startMillis = getSelectedTimeInMillis();
2581 int flags = DateUtils.FORMAT_SHOW_TIME
2582 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
2583 | DateUtils.FORMAT_SHOW_WEEKDAY;
2584 final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
2585 menu.setHeaderTitle(title);
2587 int numSelectedEvents = mSelectedEvents.size();
2588 if (mNumDays == 1) {
2591 // If there is a selected event, then allow it to be viewed and
2593 if (numSelectedEvents >= 1) {
2594 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2595 item.setOnMenuItemClickListener(mContextMenuHandler);
2596 item.setIcon(android.R.drawable.ic_menu_info_details);
2598 if (isEventEditable(mContext, mSelectedEvent)) {
2599 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2600 item.setOnMenuItemClickListener(mContextMenuHandler);
2601 item.setIcon(android.R.drawable.ic_menu_edit);
2602 item.setAlphabeticShortcut('e');
2604 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2605 item.setOnMenuItemClickListener(mContextMenuHandler);
2606 item.setIcon(android.R.drawable.ic_menu_delete);
2609 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2610 item.setOnMenuItemClickListener(mContextMenuHandler);
2611 item.setIcon(android.R.drawable.ic_menu_add);
2612 item.setAlphabeticShortcut('n');
2614 // Otherwise, if the user long-pressed on a blank hour, allow
2615 // them to create an event. They can also do this by tapping.
2616 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2617 item.setOnMenuItemClickListener(mContextMenuHandler);
2618 item.setIcon(android.R.drawable.ic_menu_add);
2619 item.setAlphabeticShortcut('n');
2624 // If there is a selected event, then allow it to be viewed and
2626 if (numSelectedEvents >= 1) {
2627 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2628 item.setOnMenuItemClickListener(mContextMenuHandler);
2629 item.setIcon(android.R.drawable.ic_menu_info_details);
2631 if (isEventEditable(mContext, mSelectedEvent)) {
2632 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2633 item.setOnMenuItemClickListener(mContextMenuHandler);
2634 item.setIcon(android.R.drawable.ic_menu_edit);
2635 item.setAlphabeticShortcut('e');
2637 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2638 item.setOnMenuItemClickListener(mContextMenuHandler);
2639 item.setIcon(android.R.drawable.ic_menu_delete);
2642 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2643 item.setOnMenuItemClickListener(mContextMenuHandler);
2644 item.setIcon(android.R.drawable.ic_menu_add);
2645 item.setAlphabeticShortcut('n');
2647 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2648 item.setOnMenuItemClickListener(mContextMenuHandler);
2649 item.setIcon(android.R.drawable.ic_menu_day);
2650 item.setAlphabeticShortcut('d');
2652 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2653 item.setOnMenuItemClickListener(mContextMenuHandler);
2654 item.setIcon(android.R.drawable.ic_menu_agenda);
2655 item.setAlphabeticShortcut('a');
2657 // No events are selected
2658 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2659 item.setOnMenuItemClickListener(mContextMenuHandler);
2660 item.setIcon(android.R.drawable.ic_menu_add);
2661 item.setAlphabeticShortcut('n');
2663 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2664 item.setOnMenuItemClickListener(mContextMenuHandler);
2665 item.setIcon(android.R.drawable.ic_menu_day);
2666 item.setAlphabeticShortcut('d');
2668 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2669 item.setOnMenuItemClickListener(mContextMenuHandler);
2670 item.setIcon(android.R.drawable.ic_menu_agenda);
2671 item.setAlphabeticShortcut('a');
2678 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
2679 public boolean onMenuItemClick(MenuItem item) {
2680 switch (item.getItemId()) {
2681 case MenuHelper.MENU_EVENT_VIEW: {
2682 if (mSelectedEvent != null) {
2683 long id = mSelectedEvent.id;
2684 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2685 Intent intent = new Intent(Intent.ACTION_VIEW);
2686 intent.setData(eventUri);
2687 intent.setClassName(mContext, EventInfoActivity.class.getName());
2688 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2689 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2690 mParentActivity.startActivity(intent);
2694 case MenuHelper.MENU_EVENT_EDIT: {
2695 if (mSelectedEvent != null) {
2696 long id = mSelectedEvent.id;
2697 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2698 Intent intent = new Intent(Intent.ACTION_EDIT);
2699 intent.setData(eventUri);
2700 intent.setClassName(mContext, EditEvent.class.getName());
2701 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2702 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2703 mParentActivity.startActivity(intent);
2707 case MenuHelper.MENU_DAY: {
2708 long startMillis = getSelectedTimeInMillis();
2709 Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
2712 case MenuHelper.MENU_AGENDA: {
2713 long startMillis = getSelectedTimeInMillis();
2714 Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
2717 case MenuHelper.MENU_EVENT_CREATE: {
2718 long startMillis = getSelectedTimeInMillis();
2719 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
2720 Intent intent = new Intent(Intent.ACTION_VIEW);
2721 intent.setClassName(mContext, EditEvent.class.getName());
2722 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
2723 intent.putExtra(EVENT_END_TIME, endMillis);
2724 intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
2725 mParentActivity.startActivity(intent);
2728 case MenuHelper.MENU_EVENT_DELETE: {
2729 if (mSelectedEvent != null) {
2730 Event selectedEvent = mSelectedEvent;
2731 long begin = selectedEvent.startMillis;
2732 long end = selectedEvent.endMillis;
2733 long id = selectedEvent.id;
2734 mDeleteEventHelper.delete(begin, end, id, -1);
2746 private static boolean isEventEditable(Context context, Event e) {
2747 ContentResolver cr = context.getContentResolver();
2749 int visibility = Calendars.NO_ACCESS;
2750 int relationship = Attendees.RELATIONSHIP_ORGANIZER;
2752 // Get the calendar id for this event
2753 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
2754 new String[] { Events.CALENDAR_ID },
2755 null /* selection */,
2756 null /* selectionArgs */,
2758 if ((cursor == null) || (cursor.getCount() == 0)) {
2761 cursor.moveToFirst();
2762 long calId = cursor.getLong(0);
2763 cursor.deactivate();
2765 Uri uri = Calendars.CONTENT_URI;
2766 String where = String.format(CALENDARS_WHERE, calId);
2767 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
2769 if (cursor != null) {
2770 cursor.moveToFirst();
2771 visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
2776 uri = Attendees.CONTENT_URI;
2777 where = String.format(ATTENDEES_WHERE, e.id);
2778 Cursor attendeesCursor = cr.query(uri, ATTENDEES_PROJECTION, where, null, null);
2779 if (attendeesCursor != null) {
2780 if (attendeesCursor.moveToFirst()) {
2781 relationship = attendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
2783 attendeesCursor.close();
2786 return visibility >= Calendars.CONTRIBUTOR_ACCESS &&
2787 relationship >= Attendees.RELATIONSHIP_ORGANIZER;
2791 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
2792 * If the touch position is not within the displayed grid, then this
2793 * method returns false.
2795 * @param x the x position of the touch
2796 * @param y the y position of the touch
2797 * @return true if the touch position is valid
2799 private boolean setSelectionFromPosition(int x, int y) {
2800 if (x < mHoursWidth) {
2804 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
2805 if (day >= mNumDays) {
2808 day += mFirstJulianDay;
2810 if (y < mFirstCell + mFirstHourOffset) {
2811 mSelectionAllDay = true;
2813 hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
2815 mSelectionHour = hour;
2816 mSelectionAllDay = false;
2818 mSelectionDay = day;
2819 findSelectedEvent(x, y);
2820 // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
2821 // + " hour: " + hour
2822 // + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
2823 // if (mSelectedEvent != null) {
2824 // Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
2825 // for (Event ev : mSelectedEvents) {
2826 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2827 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2828 // String timeRange = formatDateRange(mParentActivity,
2829 // ev.startMillis, ev.endMillis, flags);
2831 // Log.i("Cal", " " + timeRange + " " + ev.title);
2837 private void findSelectedEvent(int x, int y) {
2838 int date = mSelectionDay;
2839 int cellWidth = mCellWidth;
2840 ArrayList<Event> events = mEvents;
2841 int numEvents = events.size();
2842 int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
2844 mSelectedEvent = null;
2846 mSelectedEvents.clear();
2847 if (mSelectionAllDay) {
2849 float minYdistance = 10000.0f; // any large number
2850 Event closestEvent = null;
2851 float drawHeight = mAllDayHeight;
2852 int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
2853 for (int i = 0; i < numEvents; i++) {
2854 Event event = events.get(i);
2855 if (!event.allDay) {
2859 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
2860 float numRectangles = event.getMaxColumns();
2861 float height = drawHeight / numRectangles;
2862 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
2863 height = MAX_ALLDAY_EVENT_HEIGHT;
2865 float eventTop = yOffset + height * event.getColumn();
2866 float eventBottom = eventTop + height;
2867 if (eventTop < y && eventBottom > y) {
2868 // If the touch is inside the event rectangle, then
2870 mSelectedEvents.add(event);
2871 closestEvent = event;
2874 // Find the closest event
2875 if (eventTop >= y) {
2876 yDistance = eventTop - y;
2878 yDistance = y - eventBottom;
2880 if (yDistance < minYdistance) {
2881 minYdistance = yDistance;
2882 closestEvent = event;
2887 mSelectedEvent = closestEvent;
2891 // Adjust y for the scrollable bitmap
2892 y += mViewStartY - mFirstCell;
2894 // Use a region around (x,y) for the selection region
2895 Rect region = mRect;
2896 region.left = x - 10;
2897 region.right = x + 10;
2898 region.top = y - 10;
2899 region.bottom = y + 10;
2901 EventGeometry geometry = mEventGeometry;
2903 for (int i = 0; i < numEvents; i++) {
2904 Event event = events.get(i);
2905 // Compute the event rectangle.
2906 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
2910 // If the event intersects the selection region, then add it to
2912 if (geometry.eventIntersectsSelection(event, region)) {
2913 mSelectedEvents.add(event);
2917 // If there are any events in the selected region, then assign the
2918 // closest one to mSelectedEvent.
2919 if (mSelectedEvents.size() > 0) {
2920 int len = mSelectedEvents.size();
2921 Event closestEvent = null;
2922 float minDist = mViewWidth + mViewHeight; // some large distance
2923 for (int index = 0; index < len; index++) {
2924 Event ev = mSelectedEvents.get(index);
2925 float dist = geometry.pointToEvent(x, y, ev);
2926 if (dist < minDist) {
2931 mSelectedEvent = closestEvent;
2933 // Keep the selected hour and day consistent with the selected
2934 // event. They could be different if we touched on an empty hour
2935 // slot very close to an event in the previous hour slot. In
2936 // that case we will select the nearby event.
2937 int startDay = mSelectedEvent.startDay;
2938 int endDay = mSelectedEvent.endDay;
2939 if (mSelectionDay < startDay) {
2940 mSelectionDay = startDay;
2941 } else if (mSelectionDay > endDay) {
2942 mSelectionDay = endDay;
2945 int startHour = mSelectedEvent.startTime / 60;
2947 if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
2948 endHour = (mSelectedEvent.endTime - 1) / 60;
2950 endHour = mSelectedEvent.endTime / 60;
2953 if (mSelectionHour < startHour) {
2954 mSelectionHour = startHour;
2955 } else if (mSelectionHour > endHour) {
2956 mSelectionHour = endHour;
2961 // Encapsulates the code to continue the scrolling after the
2962 // finger is lifted. Instead of stopping the scroll immediately,
2963 // the scroll continues to "free spin" and gradually slows down.
2964 private class ContinueScroll implements Runnable {
2969 private static final float FRICTION_COEF = 0.7F;
2970 private static final long FREE_SPIN_MILLIS = 180;
2971 private static final int MAX_DELTA = 60;
2972 private static final int SCROLL_REPEAT_INTERVAL = 30;
2974 public void init(int deltaY) {
2978 } else if (deltaY < 0) {
2981 mAbsDeltaY = Math.abs(deltaY);
2983 // Limit the maximum speed
2984 if (mAbsDeltaY > MAX_DELTA) {
2985 mAbsDeltaY = MAX_DELTA;
2987 mFloatDeltaY = mAbsDeltaY;
2988 mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
2989 // Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
2990 // + " mViewStartY: " + mViewStartY);
2994 long time = System.currentTimeMillis();
2996 // Start out with a frictionless "free spin"
2997 if (time > mFreeSpinTime) {
2998 // If the delta is small, then apply a fixed deceleration.
3000 if (mAbsDeltaY <= 10) {
3003 mFloatDeltaY *= FRICTION_COEF;
3004 mAbsDeltaY = (int) mFloatDeltaY;
3007 if (mAbsDeltaY < 0) {
3012 if (mSignDeltaY == 1) {
3013 mViewStartY -= mAbsDeltaY;
3015 mViewStartY += mAbsDeltaY;
3017 // Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
3018 // + " mViewStartY: " + mViewStartY);
3020 if (mViewStartY < 0) {
3023 } else if (mViewStartY > mMaxViewStartY) {
3024 mViewStartY = mMaxViewStartY;
3030 if (mAbsDeltaY > 0) {
3031 postDelayed(this, SCROLL_REPEAT_INTERVAL);
3035 resetSelectedHour();
3036 mRedrawScreen = true;
3044 * Cleanup the pop-up.
3046 public void cleanup() {
3047 // Protect against null-pointer exceptions
3048 if (mPopup != null) {
3051 Handler handler = getHandler();
3052 if (handler != null) {
3053 handler.removeCallbacks(mDismissPopup);
3058 mRedrawScreen = false;
3061 @Override protected void onDetachedFromWindow() {
3063 if (mBitmap != null) {
3067 super.onDetachedFromWindow();
3070 class DismissPopup implements Runnable {
3072 // Protect against null-pointer exceptions
3073 if (mPopup != null) {