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.TextUtils;
46 import android.text.format.DateFormat;
47 import android.text.format.DateUtils;
48 import android.text.format.Time;
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
78 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
80 private boolean mOnFlingCalled;
82 * ID of the last event which was displayed with the toast popup.
84 * This is used to prevent popping up multiple quick views for the same event, especially
85 * during calendar syncs. This becomes valid when an event is selected, either by default
86 * on starting calendar or by scrolling to an event. It becomes invalid when the user
87 * explicitly scrolls to an empty time slot, changes views, or deletes the event.
89 private long mLastPopupEventID;
91 protected CalendarApplication mCalendarApp;
92 protected CalendarActivity mParentActivity;
94 private static final String[] CALENDARS_PROJECTION = new String[] {
96 Calendars.ACCESS_LEVEL, // 1
97 Calendars.OWNER_ACCOUNT, // 2
99 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
100 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
101 private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
103 private static final String[] ATTENDEES_PROJECTION = new String[] {
105 Attendees.ATTENDEE_RELATIONSHIP, // 1
107 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
108 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
110 private static float SMALL_ROUND_RADIUS = 3.0F;
112 private static final int FROM_NONE = 0;
113 private static final int FROM_ABOVE = 1;
114 private static final int FROM_BELOW = 2;
115 private static final int FROM_LEFT = 4;
116 private static final int FROM_RIGHT = 8;
118 private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
120 private ContinueScroll mContinueScroll = new ContinueScroll();
122 static private class DayHeader{
127 private DayHeader[] dayHeaders = new DayHeader[32];
129 // Make this visible within the package for more informative debugging
131 private Time mCurrentTime;
132 //Update the current time line every five minutes if the window is left open that long
133 private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
134 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
135 private int mTodayJulianDay;
137 private Typeface mBold = Typeface.DEFAULT_BOLD;
138 private int mFirstJulianDay;
139 private int mLastJulianDay;
141 private int mMonthLength;
142 private int mFirstDate;
143 private int[] mEarliestStartHour; // indexed by the week day offset
144 private boolean[] mHasAllDayEvent; // indexed by the week day offset
146 private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
149 * This variable helps to avoid unnecessarily reloading events by keeping
150 * track of the start millis parameter used for the most recent loading
151 * of events. If the next reload matches this, then the events are not
152 * reloaded. To force a reload, set this to zero (this is set to zero
153 * in the method clearCachedEvents()).
155 private long mLastReloadMillis;
157 private ArrayList<Event> mEvents = new ArrayList<Event>();
158 private int mSelectionDay; // Julian day
159 private int mSelectionHour;
161 /* package private so that CalendarActivity can read it when creating new
164 boolean mSelectionAllDay;
166 private int mCellWidth;
168 // Pre-allocate these objects and re-use them
169 private Rect mRect = new Rect();
170 private RectF mRectF = new RectF();
171 private Rect mSrcRect = new Rect();
172 private Rect mDestRect = new Rect();
173 private Paint mPaint = new Paint();
174 private Paint mPaintBorder = new Paint();
175 private Paint mEventTextPaint = new Paint();
176 private Paint mSelectionPaint = new Paint();
177 private Path mPath = new Path();
179 protected boolean mDrawTextInEventRect;
180 private int mStartDay;
182 private PopupWindow mPopup;
183 private View mPopupView;
185 // The number of milliseconds to show the popup window
186 private static final int POPUP_DISMISS_DELAY = 3000;
187 private DismissPopup mDismissPopup = new DismissPopup();
189 // For drawing to an off-screen Canvas
190 private Bitmap mBitmap;
191 private Canvas mCanvas;
192 private boolean mRedrawScreen = true;
193 private boolean mRemeasure = true;
195 private final EventLoader mEventLoader;
196 protected final EventGeometry mEventGeometry;
198 private static final int DAY_GAP = 1;
199 private static final int HOUR_GAP = 1;
200 private static int SINGLE_ALLDAY_HEIGHT = 20;
201 private static int MAX_ALLDAY_HEIGHT = 72;
202 private static int ALLDAY_TOP_MARGIN = 3;
203 private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
205 /* The extra space to leave above the text in all-day events */
206 private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
208 /* The extra space to leave above the text in normal events */
209 private static final int NORMAL_TEXT_TOP_MARGIN = 2;
211 private static final int HOURS_LEFT_MARGIN = 2;
212 private static final int HOURS_RIGHT_MARGIN = 4;
213 private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
215 private static int CURRENT_TIME_LINE_HEIGHT = 2;
216 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
217 private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6;
218 private static int CURRENT_TIME_MARKER_HEIGHT = 6;
219 private static int CURRENT_TIME_MARKER_WIDTH = 8;
220 private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1;
222 /* package */ static final int MINUTES_PER_HOUR = 60;
223 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
224 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
225 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
226 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
228 private static int NORMAL_FONT_SIZE = 12;
229 private static int EVENT_TEXT_FONT_SIZE = 12;
230 private static int HOURS_FONT_SIZE = 12;
231 private static int AMPM_FONT_SIZE = 9;
232 private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
233 private static final int MAX_EVENT_TEXT_LEN = 500;
234 private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels
236 private static int mSelectionColor;
237 private static int mPressedColor;
238 private static int mSelectedEventTextColor;
239 private static int mEventTextColor;
240 private static int mWeek_saturdayColor;
241 private static int mWeek_sundayColor;
242 private static int mCalendarDateBannerTextColor;
243 private static int mCalendarAllDayBackground;
244 private static int mCalendarAmPmLabel;
245 private static int mCalendarDateBannerBackground;
246 private static int mCalendarDateSelected;
247 private static int mCalendarGridAreaBackground;
248 private static int mCalendarGridAreaSelected;
249 private static int mCalendarGridLineHorizontalColor;
250 private static int mCalendarGridLineVerticalColor;
251 private static int mCalendarHourBackground;
252 private static int mCalendarHourLabel;
253 private static int mCalendarHourSelected;
254 private static int mCurrentTimeMarkerColor;
255 private static int mCurrentTimeMarkerBorderColor;
257 private int mViewStartX;
258 private int mViewStartY;
259 private int mMaxViewStartY;
260 private int mBitmapHeight;
261 private int mViewHeight;
262 private int mViewWidth;
263 private int mGridAreaHeight;
264 private int mCellHeight;
265 private int mScrollStartY;
266 private int mPreviousDirection;
267 private int mPreviousDistanceX;
269 private int mHoursTextHeight;
270 private int mEventTextAscent;
271 private int mEventTextHeight;
272 private int mAllDayHeight;
273 private int mBannerPlusMargin;
274 private int mMaxAllDayEvents;
276 protected int mNumDays = 7;
277 private int mNumHours = 10;
278 private int mHoursWidth;
279 private int mDateStrWidth;
280 private int mFirstCell;
281 private int mFirstHour = -1;
282 private int mFirstHourOffset;
283 private String[] mHourStrs;
284 private String[] mDayStrs;
285 private String[] mDayStrs2Letter;
286 private boolean mIs24HourFormat;
288 private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
289 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
290 private boolean mComputeSelectedEvents;
291 private Event mSelectedEvent;
292 private Event mPrevSelectedEvent;
293 private Rect mPrevBox = new Rect();
294 protected final Resources mResources;
295 private String mAmString;
296 private String mPmString;
297 private DeleteEventHelper mDeleteEventHelper;
299 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
302 * The initial state of the touch mode when we enter this view.
304 private static final int TOUCH_MODE_INITIAL_STATE = 0;
307 * Indicates we just received the touch event and we are waiting to see if
308 * it is a tap or a scroll gesture.
310 private static final int TOUCH_MODE_DOWN = 1;
313 * Indicates the touch gesture is a vertical scroll
315 private static final int TOUCH_MODE_VSCROLL = 0x20;
318 * Indicates the touch gesture is a horizontal scroll
320 private static final int TOUCH_MODE_HSCROLL = 0x40;
322 private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
325 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
327 private static final int SELECTION_HIDDEN = 0;
328 private static final int SELECTION_PRESSED = 1;
329 private static final int SELECTION_SELECTED = 2;
330 private static final int SELECTION_LONGPRESS = 3;
332 private int mSelectionMode = SELECTION_HIDDEN;
334 private boolean mScrolling = false;
336 private String mDateRange;
337 private TextView mTitleTextView;
339 public CalendarView(CalendarActivity activity) {
342 mScale = getContext().getResources().getDisplayMetrics().density;
344 SINGLE_ALLDAY_HEIGHT *= mScale;
345 MAX_ALLDAY_HEIGHT *= mScale;
346 ALLDAY_TOP_MARGIN *= mScale;
347 MAX_ALLDAY_EVENT_HEIGHT *= mScale;
349 NORMAL_FONT_SIZE *= mScale;
350 EVENT_TEXT_FONT_SIZE *= mScale;
351 HOURS_FONT_SIZE *= mScale;
352 AMPM_FONT_SIZE *= mScale;
353 MIN_CELL_WIDTH_FOR_TEXT *= mScale;
354 MIN_EVENT_HEIGHT *= mScale;
356 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
358 CURRENT_TIME_MARKER_HEIGHT *= mScale;
359 CURRENT_TIME_MARKER_WIDTH *= mScale;
360 CURRENT_TIME_LINE_HEIGHT *= mScale;
361 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
362 CURRENT_TIME_MARKER_INNER_WIDTH *= mScale;
363 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;
365 SMALL_ROUND_RADIUS *= mScale;
369 mResources = activity.getResources();
370 mEventLoader = activity.mEventLoader;
371 mEventGeometry = new EventGeometry();
372 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
373 mEventGeometry.setHourGap(HOUR_GAP);
374 mParentActivity = activity;
375 mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
376 mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
377 mLastPopupEventID = INVALID_EVENT_ID;
382 private void init(Context context) {
385 // Allow focus in touch mode so that we can do keyboard shortcuts
386 // even after we've entered touch mode.
387 setFocusableInTouchMode(true);
389 setOnCreateContextMenuListener(this);
391 mStartDay = Utils.getFirstDayOfWeek();
393 mCurrentTime = new Time();
394 long currentTime = System.currentTimeMillis();
395 mCurrentTime.set(currentTime);
396 //The % makes it go off at the next increment of 5 minutes.
397 postDelayed(mUpdateCurrentTime,
398 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
399 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
401 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
402 mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
403 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
404 mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
405 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
406 mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
407 mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
408 mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
409 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
410 mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
411 mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
412 mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
413 mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
414 mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
415 mSelectionColor = mResources.getColor(R.color.selection);
416 mPressedColor = mResources.getColor(R.color.pressed);
417 mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
418 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
419 mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker);
420 mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border);
421 mEventTextPaint.setColor(mEventTextColor);
422 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
423 mEventTextPaint.setTextAlign(Paint.Align.LEFT);
424 mEventTextPaint.setAntiAlias(true);
426 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
427 Paint p = mSelectionPaint;
428 p.setColor(gridLineColor);
429 p.setStyle(Style.STROKE);
430 p.setStrokeWidth(2.0f);
431 p.setAntiAlias(false);
434 p.setAntiAlias(true);
436 mPaintBorder.setColor(0xffc8c8c8);
437 mPaintBorder.setStyle(Style.STROKE);
438 mPaintBorder.setAntiAlias(true);
439 mPaintBorder.setStrokeWidth(2.0f);
441 // Allocate space for 2 weeks worth of weekday names so that we can
442 // easily start the week display at any week day.
443 mDayStrs = new String[14];
445 // Also create an array of 2-letter abbreviations.
446 mDayStrs2Letter = new String[14];
448 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
449 int index = i - Calendar.SUNDAY;
450 // e.g. Tue for Tuesday
451 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
452 mDayStrs[index + 7] = mDayStrs[index];
453 // e.g. Tu for Tuesday
454 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
456 // If we don't have 2-letter day strings, fall back to 1-letter.
457 if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
458 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
461 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
464 // Figure out how much space we need for the 3-letter abbrev names
465 // in the worst case.
466 p.setTextSize(NORMAL_FONT_SIZE);
467 p.setTypeface(mBold);
468 String[] dateStrs = {" 28", " 30"};
469 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
470 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
472 p.setTextSize(HOURS_FONT_SIZE);
474 mIs24HourFormat = DateFormat.is24HourFormat(context);
475 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
476 mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
478 mAmString = DateUtils.getAMPMString(Calendar.AM);
479 mPmString = DateUtils.getAMPMString(Calendar.PM);
480 String[] ampm = {mAmString, mPmString};
481 p.setTextSize(AMPM_FONT_SIZE);
482 mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
483 mHoursWidth += HOURS_MARGIN;
485 LayoutInflater inflater;
486 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
487 mPopupView = inflater.inflate(R.layout.bubble_event, null);
488 mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
489 ViewGroup.LayoutParams.MATCH_PARENT,
490 ViewGroup.LayoutParams.WRAP_CONTENT));
491 mPopup = new PopupWindow(context);
492 mPopup.setContentView(mPopupView);
493 Resources.Theme dialogTheme = getResources().newTheme();
494 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
495 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
496 android.R.attr.windowBackground });
497 mPopup.setBackgroundDrawable(ta.getDrawable(0));
500 // Enable touching the popup window
501 mPopupView.setOnClickListener(this);
503 mBaseDate = new Time();
504 long millis = System.currentTimeMillis();
505 mBaseDate.set(millis);
507 mEarliestStartHour = new int[mNumDays];
508 mHasAllDayEvent = new boolean[mNumDays];
510 mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
511 mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
515 * This is called when the popup window is pressed.
517 public void onClick(View v) {
518 if (v == mPopupView) {
519 // Pretend it was a trackball click because that will always
520 // jump to the "View event" screen.
521 switchViews(true /* trackball */);
526 * Returns the start of the selected time in milliseconds since the epoch.
528 * @return selected time in UTC milliseconds since the epoch.
530 long getSelectedTimeInMillis() {
531 Time time = new Time(mBaseDate);
532 time.setJulianDay(mSelectionDay);
533 time.hour = mSelectionHour;
535 // We ignore the "isDst" field because we want normalize() to figure
536 // out the correct DST value and not adjust the selected time based
537 // on the current setting of DST.
538 return time.normalize(true /* ignore isDst */);
541 Time getSelectedTime() {
542 Time time = new Time(mBaseDate);
543 time.setJulianDay(mSelectionDay);
544 time.hour = mSelectionHour;
546 // We ignore the "isDst" field because we want normalize() to figure
547 // out the correct DST value and not adjust the selected time based
548 // on the current setting of DST.
549 time.normalize(true /* ignore isDst */);
554 * Returns the start of the selected time in minutes since midnight,
555 * local time. The derived class must ensure that this is consistent
556 * with the return value from getSelectedTimeInMillis().
558 int getSelectedMinutesSinceMidnight() {
559 return mSelectionHour * MINUTES_PER_HOUR;
562 public void setSelectedDay(Time time) {
564 mSelectionHour = mBaseDate.hour;
565 mSelectedEvent = null;
566 mPrevSelectedEvent = null;
567 long millis = mBaseDate.toMillis(false /* use isDst */);
568 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
569 mSelectedEvents.clear();
570 mComputeSelectedEvents = true;
572 // Force a recalculation of the first visible hour
575 mTitleTextView.setText(mDateRange);
577 // Force a redraw of the selection box.
578 mSelectionMode = SELECTION_SELECTED;
579 mRedrawScreen = true;
584 public Time getSelectedDay() {
585 Time time = new Time(mBaseDate);
586 time.setJulianDay(mSelectionDay);
587 time.hour = mSelectionHour;
589 // We ignore the "isDst" field because we want normalize() to figure
590 // out the correct DST value and not adjust the selected time based
591 // on the current setting of DST.
592 time.normalize(true /* ignore isDst */);
596 private void recalc() {
597 // Set the base date to the beginning of the week if we are displaying
600 int dayOfWeek = mBaseDate.weekDay;
601 int diff = dayOfWeek - mStartDay;
606 mBaseDate.monthDay -= diff;
607 mBaseDate.normalize(true /* ignore isDst */);
611 final long start = mBaseDate.toMillis(false /* use isDst */);
613 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
614 mLastJulianDay = mFirstJulianDay + mNumDays - 1;
616 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
617 mFirstDate = mBaseDate.monthDay;
619 int flags = DateUtils.FORMAT_SHOW_YEAR;
620 if (DateFormat.is24HourFormat(mParentActivity)) {
621 flags |= DateUtils.FORMAT_24HOUR;
624 mBaseDate.monthDay += mNumDays - 1;
625 end = mBaseDate.toMillis(true /* ignore isDst */);
626 mBaseDate.monthDay -= mNumDays - 1;
627 flags |= DateUtils.FORMAT_NO_MONTH_DAY;
629 flags |= DateUtils.FORMAT_SHOW_WEEKDAY
630 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
633 mDateRange = DateUtils.formatDateRange(mParentActivity, start, end, flags);
634 // Do not set the title here because this is called when executing
635 // initNextView() to prepare the Day view when sliding the finger
636 // horizontally but we don't always want to change the title. And
637 // if we change the title here and then change it back in the caller
638 // then we get an annoying flicker.
641 void setDetailedView(String detailedView) {
642 mDetailedView = detailedView;
646 protected void onSizeChanged(int width, int height, int oldw, int oldh) {
648 mViewHeight = height;
649 int gridAreaWidth = width - mHoursWidth;
650 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
652 Paint p = new Paint();
653 p.setTextSize(NORMAL_FONT_SIZE);
654 int bannerTextHeight = (int) Math.abs(p.ascent());
656 p.setTextSize(HOURS_FONT_SIZE);
657 mHoursTextHeight = (int) Math.abs(p.ascent());
659 p.setTextSize(EVENT_TEXT_FONT_SIZE);
660 float ascent = -p.ascent();
661 mEventTextAscent = (int) Math.ceil(ascent);
662 float totalHeight = ascent + p.descent();
663 mEventTextHeight = (int) Math.ceil(totalHeight);
666 mBannerPlusMargin = bannerTextHeight + 14;
668 mBannerPlusMargin = 0;
671 remeasure(width, height);
674 // Measures the space needed for various parts of the view after
675 // loading new events. This can change if there are all-day events.
676 private void remeasure(int width, int height) {
678 // First, clear the array of earliest start times, and the array
679 // indicating presence of an all-day event.
680 for (int day = 0; day < mNumDays; day++) {
681 mEarliestStartHour[day] = 25; // some big number
682 mHasAllDayEvent[day] = false;
685 // Compute the space needed for the all-day events, if any.
686 // Make a pass over all the events, and keep track of the maximum
687 // number of all-day events in any one day. Also, keep track of
688 // the earliest event in each day.
689 int maxAllDayEvents = 0;
690 ArrayList<Event> events = mEvents;
691 int len = events.size();
692 for (int ii = 0; ii < len; ii++) {
693 Event event = events.get(ii);
694 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
697 int max = event.getColumn() + 1;
698 if (maxAllDayEvents < max) {
699 maxAllDayEvents = max;
701 int daynum = event.startDay - mFirstJulianDay;
702 int durationDays = event.endDay - event.startDay + 1;
704 durationDays += daynum;
707 if (daynum + durationDays > mNumDays) {
708 durationDays = mNumDays - daynum;
710 for (int day = daynum; durationDays > 0; day++, durationDays--) {
711 mHasAllDayEvent[day] = true;
714 int daynum = event.startDay - mFirstJulianDay;
715 int hour = event.startTime / 60;
716 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
717 mEarliestStartHour[daynum] = hour;
720 // Also check the end hour in case the event spans more than
722 daynum = event.endDay - mFirstJulianDay;
723 hour = event.endTime / 60;
724 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
725 mEarliestStartHour[daynum] = hour;
729 mMaxAllDayEvents = maxAllDayEvents;
731 mFirstCell = mBannerPlusMargin;
732 int allDayHeight = 0;
733 if (maxAllDayEvents > 0) {
734 // If there is at most one all-day event per day, then use less
735 // space (but more than the space for a single event).
736 if (maxAllDayEvents == 1) {
737 allDayHeight = SINGLE_ALLDAY_HEIGHT;
739 // Allow the all-day area to grow in height depending on the
740 // number of all-day events we need to show, up to a limit.
741 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
742 if (allDayHeight > MAX_ALLDAY_HEIGHT) {
743 allDayHeight = MAX_ALLDAY_HEIGHT;
746 mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
748 mSelectionAllDay = false;
750 mAllDayHeight = allDayHeight;
752 mGridAreaHeight = height - mFirstCell;
753 mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
754 int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
755 int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
756 mEventGeometry.setHourHeight(mCellHeight);
758 // Create an off-screen bitmap that we can draw into.
759 mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
760 if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
762 if (mBitmap != null) {
765 mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
766 mCanvas = new Canvas(mBitmap);
768 mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
770 if (mFirstHour == -1) {
772 mFirstHourOffset = 0;
775 // When we change the base date, the number of all-day events may
776 // change and that changes the cell height. When we switch dates,
777 // we use the mFirstHourOffset from the previous view, but that may
778 // be too large for the new view if the cell height is smaller.
779 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
780 mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
782 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
784 int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
785 //When we get new events we don't want to dismiss the popup unless the event changes
786 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
789 mPopup.setWidth(eventAreaWidth - 20);
790 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
794 * Initialize the state for another view. The given view is one that has
795 * its own bitmap and will use an animation to replace the current view.
796 * The current view and new view are either both Week views or both Day
797 * views. They differ in their base date.
799 * @param view the view to initialize.
801 private void initView(CalendarView view) {
802 view.mSelectionHour = mSelectionHour;
803 view.mSelectedEvents.clear();
804 view.mComputeSelectedEvents = true;
805 view.mFirstHour = mFirstHour;
806 view.mFirstHourOffset = mFirstHourOffset;
807 view.remeasure(getWidth(), getHeight());
809 view.mSelectedEvent = null;
810 view.mPrevSelectedEvent = null;
811 view.mStartDay = mStartDay;
812 if (view.mEvents.size() > 0) {
813 view.mSelectionAllDay = mSelectionAllDay;
815 view.mSelectionAllDay = false;
818 // Redraw the screen so that the selection box will be redrawn. We may
819 // have scrolled to a different part of the day in some other view
820 // so the selection box in this view may no longer be visible.
821 view.mRedrawScreen = true;
826 * Switch to another view based on what was selected (an event or a free
827 * slot) and how it was selected (by touch or by trackball).
829 * @param trackBallSelection true if the selection was made using the
832 private void switchViews(boolean trackBallSelection) {
833 Event selectedEvent = mSelectedEvent;
836 mLastPopupEventID = INVALID_EVENT_ID;
838 // This is the Week view.
839 // With touch, we always switch to Day/Agenda View
840 // With track ball, if we selected a free slot, then create an event.
841 // If we selected a specific event, switch to EventInfo view.
842 if (trackBallSelection) {
843 if (selectedEvent == null) {
844 // Switch to the EditEvent view
845 long startMillis = getSelectedTimeInMillis();
846 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
847 Intent intent = new Intent(Intent.ACTION_VIEW);
848 intent.setClassName(mParentActivity, EditEvent.class.getName());
849 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
850 intent.putExtra(EVENT_END_TIME, endMillis);
851 mParentActivity.startActivity(intent);
853 // Switch to the EventInfo view
854 Intent intent = new Intent(Intent.ACTION_VIEW);
855 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
857 intent.setData(eventUri);
858 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
859 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
860 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
861 mParentActivity.startActivity(intent);
864 // This was a touch selection. If the touch selected a single
865 // unambiguous event, then view that event. Otherwise go to
867 if (mSelectedEvents.size() == 1) {
868 // Switch to the EventInfo view
869 Intent intent = new Intent(Intent.ACTION_VIEW);
870 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
872 intent.setData(eventUri);
873 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
874 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
875 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
876 mParentActivity.startActivity(intent);
878 // Switch to the Day/Agenda view.
879 long millis = getSelectedTimeInMillis();
880 Utils.startActivity(mParentActivity, mDetailedView, millis);
884 // This is the Day view.
885 // If we selected a free slot, then create an event.
886 // If we selected an event, then go to the EventInfo view.
887 if (selectedEvent == null) {
888 // Switch to the EditEvent view
889 long startMillis = getSelectedTimeInMillis();
890 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
891 Intent intent = new Intent(Intent.ACTION_VIEW);
892 intent.setClassName(mParentActivity, EditEvent.class.getName());
893 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
894 intent.putExtra(EVENT_END_TIME, endMillis);
895 mParentActivity.startActivity(intent);
897 // Switch to the EventInfo view
898 Intent intent = new Intent(Intent.ACTION_VIEW);
899 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
900 intent.setData(eventUri);
901 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
902 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
903 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
904 mParentActivity.startActivity(intent);
910 public boolean onKeyUp(int keyCode, KeyEvent event) {
912 long duration = event.getEventTime() - event.getDownTime();
915 case KeyEvent.KEYCODE_DPAD_CENTER:
916 if (mSelectionMode == SELECTION_HIDDEN) {
917 // Don't do anything unless the selection is visible.
921 if (mSelectionMode == SELECTION_PRESSED) {
922 // This was the first press when there was nothing selected.
923 // Change the selection from the "pressed" state to the
924 // the "selected" state. We treat short-press and
925 // long-press the same here because nothing was selected.
926 mSelectionMode = SELECTION_SELECTED;
927 mRedrawScreen = true;
932 // Check the duration to determine if this was a short press
933 if (duration < ViewConfiguration.getLongPressTimeout()) {
934 switchViews(true /* trackball */);
936 mSelectionMode = SELECTION_LONGPRESS;
937 mRedrawScreen = true;
942 case KeyEvent.KEYCODE_BACK:
943 if (event.isTracking() && !event.isCanceled()) {
945 mParentActivity.finish();
950 return super.onKeyUp(keyCode, event);
954 public boolean onKeyDown(int keyCode, KeyEvent event) {
955 if (mSelectionMode == SELECTION_HIDDEN) {
956 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
957 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
958 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
959 // Display the selection box but don't move or select it
960 // on this key press.
961 mSelectionMode = SELECTION_SELECTED;
962 mRedrawScreen = true;
965 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
966 // Display the selection box but don't select it
967 // on this key press.
968 mSelectionMode = SELECTION_PRESSED;
969 mRedrawScreen = true;
975 mSelectionMode = SELECTION_SELECTED;
978 int selectionDay = mSelectionDay;
981 case KeyEvent.KEYCODE_DEL:
982 // Delete the selected event, if any
983 Event selectedEvent = mSelectedEvent;
984 if (selectedEvent == null) {
988 mLastPopupEventID = INVALID_EVENT_ID;
990 long begin = selectedEvent.startMillis;
991 long end = selectedEvent.endMillis;
992 long id = selectedEvent.id;
993 mDeleteEventHelper.delete(begin, end, id, -1);
995 case KeyEvent.KEYCODE_ENTER:
996 switchViews(true /* trackball or keyboard */);
998 case KeyEvent.KEYCODE_BACK:
999 if (event.getRepeatCount() == 0) {
1000 event.startTracking();
1003 return super.onKeyDown(keyCode, event);
1004 case KeyEvent.KEYCODE_DPAD_LEFT:
1005 if (mSelectedEvent != null) {
1006 mSelectedEvent = mSelectedEvent.nextLeft;
1008 if (mSelectedEvent == null) {
1009 mLastPopupEventID = INVALID_EVENT_ID;
1015 case KeyEvent.KEYCODE_DPAD_RIGHT:
1016 if (mSelectedEvent != null) {
1017 mSelectedEvent = mSelectedEvent.nextRight;
1019 if (mSelectedEvent == null) {
1020 mLastPopupEventID = INVALID_EVENT_ID;
1026 case KeyEvent.KEYCODE_DPAD_UP:
1027 if (mSelectedEvent != null) {
1028 mSelectedEvent = mSelectedEvent.nextUp;
1030 if (mSelectedEvent == null) {
1031 mLastPopupEventID = INVALID_EVENT_ID;
1032 if (!mSelectionAllDay) {
1033 mSelectionHour -= 1;
1034 adjustHourSelection();
1035 mSelectedEvents.clear();
1036 mComputeSelectedEvents = true;
1042 case KeyEvent.KEYCODE_DPAD_DOWN:
1043 if (mSelectedEvent != null) {
1044 mSelectedEvent = mSelectedEvent.nextDown;
1046 if (mSelectedEvent == null) {
1047 mLastPopupEventID = INVALID_EVENT_ID;
1048 if (mSelectionAllDay) {
1049 mSelectionAllDay = false;
1052 adjustHourSelection();
1053 mSelectedEvents.clear();
1054 mComputeSelectedEvents = true;
1061 return super.onKeyDown(keyCode, event);
1064 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1066 CalendarView view = mParentActivity.getNextView();
1067 Time date = view.mBaseDate;
1068 date.set(mBaseDate);
1069 if (selectionDay < mFirstJulianDay) {
1070 date.monthDay -= mNumDays;
1073 date.monthDay += mNumDays;
1076 date.normalize(true /* ignore isDst */);
1077 view.mSelectionDay = selectionDay;
1080 mTitleTextView.setText(view.mDateRange);
1081 mParentActivity.switchViews(forward, 0, 0);
1084 mSelectionDay = selectionDay;
1085 mSelectedEvents.clear();
1086 mComputeSelectedEvents = true;
1089 mRedrawScreen = true;
1094 return super.onKeyDown(keyCode, event);
1097 // This is called after scrolling stops to move the selected hour
1098 // to the visible part of the screen.
1099 private void resetSelectedHour() {
1100 if (mSelectionHour < mFirstHour + 1) {
1101 mSelectionHour = mFirstHour + 1;
1102 mSelectedEvent = null;
1103 mSelectedEvents.clear();
1104 mComputeSelectedEvents = true;
1105 } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1106 mSelectionHour = mFirstHour + mNumHours - 3;
1107 mSelectedEvent = null;
1108 mSelectedEvents.clear();
1109 mComputeSelectedEvents = true;
1113 private void initFirstHour() {
1114 mFirstHour = mSelectionHour - mNumHours / 2;
1115 if (mFirstHour < 0) {
1117 } else if (mFirstHour + mNumHours > 24) {
1118 mFirstHour = 24 - mNumHours;
1123 * Recomputes the first full hour that is visible on screen after the
1124 * screen is scrolled.
1126 private void computeFirstHour() {
1127 // Compute the first full hour that is visible on screen
1128 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1129 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1132 private void adjustHourSelection() {
1133 if (mSelectionHour < 0) {
1135 if (mMaxAllDayEvents > 0) {
1136 mPrevSelectedEvent = null;
1137 mSelectionAllDay = true;
1141 if (mSelectionHour > 23) {
1142 mSelectionHour = 23;
1145 // If the selected hour is at least 2 time slots from the top and
1146 // bottom of the screen, then don't scroll the view.
1147 if (mSelectionHour < mFirstHour + 1) {
1148 // If there are all-days events for the selected day but there
1149 // are no more normal events earlier in the day, then jump to
1150 // the all-day event area.
1151 // Exception 1: allow the user to scroll to 8am with the trackball
1152 // before jumping to the all-day event area.
1153 // Exception 2: if 12am is on screen, then allow the user to select
1154 // 12am before going up to the all-day event area.
1155 int daynum = mSelectionDay - mFirstJulianDay;
1156 if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1157 && mFirstHour > 0 && mFirstHour < 8) {
1158 mPrevSelectedEvent = null;
1159 mSelectionAllDay = true;
1160 mSelectionHour = mFirstHour + 1;
1164 if (mFirstHour > 0) {
1166 mViewStartY -= (mCellHeight + HOUR_GAP);
1167 if (mViewStartY < 0) {
1174 if (mSelectionHour > mFirstHour + mNumHours - 3) {
1175 if (mFirstHour < 24 - mNumHours) {
1177 mViewStartY += (mCellHeight + HOUR_GAP);
1178 if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
1179 mViewStartY = mBitmapHeight - mGridAreaHeight;
1182 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1183 mViewStartY = mBitmapHeight - mGridAreaHeight;
1188 void clearCachedEvents() {
1189 mLastReloadMillis = 0;
1192 private Runnable mCancelCallback = new Runnable() {
1194 clearCachedEvents();
1198 void reloadEvents() {
1199 // Protect against this being called before this view has been
1201 if (mParentActivity == null) {
1205 mSelectedEvent = null;
1206 mPrevSelectedEvent = null;
1207 mSelectedEvents.clear();
1209 // The start date is the beginning of the week at 12am
1210 Time weekStart = new Time();
1211 weekStart.set(mBaseDate);
1213 weekStart.minute = 0;
1214 weekStart.second = 0;
1215 long millis = weekStart.normalize(true /* ignore isDst */);
1217 // Avoid reloading events unnecessarily.
1218 if (millis == mLastReloadMillis) {
1221 mLastReloadMillis = millis;
1223 // load events in the background
1224 mParentActivity.startProgressSpinner();
1225 final ArrayList<Event> events = new ArrayList<Event>();
1226 mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
1230 mRedrawScreen = true;
1231 mComputeSelectedEvents = true;
1233 mParentActivity.stopProgressSpinner();
1236 }, mCancelCallback);
1240 protected void onDraw(Canvas canvas) {
1242 remeasure(getWidth(), getHeight());
1246 if (mRedrawScreen && mCanvas != null) {
1248 mRedrawScreen = false;
1251 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1253 if (mViewStartX > 0) {
1254 canvas.translate(mViewWidth - mViewStartX, 0);
1256 canvas.translate(-(mViewWidth + mViewStartX), 0);
1258 CalendarView nextView = mParentActivity.getNextView();
1260 // Prevent infinite recursive calls to onDraw().
1261 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
1263 nextView.onDraw(canvas);
1266 canvas.translate(-mViewStartX, 0);
1269 if (mBitmap != null) {
1270 drawCalendarView(canvas);
1273 // Draw the fixed areas (that don't scroll) directly to the canvas.
1274 drawAfterScroll(canvas);
1275 mComputeSelectedEvents = false;
1277 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1282 private void drawCalendarView(Canvas canvas) {
1284 // Copy the scrollable region from the big bitmap to the canvas.
1285 Rect src = mSrcRect;
1286 Rect dest = mDestRect;
1288 src.top = mViewStartY;
1289 src.bottom = mViewStartY + mGridAreaHeight;
1291 src.right = mViewWidth;
1293 dest.top = mFirstCell;
1294 dest.bottom = mViewHeight;
1296 dest.right = mViewWidth;
1299 canvas.clipRect(dest);
1300 canvas.drawColor(0, PorterDuff.Mode.CLEAR);
1301 canvas.drawBitmap(mBitmap, src, dest, null);
1305 private void drawAfterScroll(Canvas canvas) {
1309 if (mMaxAllDayEvents != 0) {
1310 drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
1311 drawUpperLeftCorner(r, canvas, p);
1315 drawDayHeaderLoop(r, canvas, p);
1318 // Draw the AM and PM indicators if we're in 12 hour mode
1319 if (!mIs24HourFormat) {
1320 drawAmPm(canvas, p);
1323 // Update the popup window showing the event details, but only if
1324 // we are not scrolling and we have focus.
1325 if (!mScrolling && isFocused()) {
1326 updateEventDetails();
1330 // This isn't really the upper-left corner. It's the square area just
1331 // below the upper-left corner, above the hours and to the left of the
1333 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
1334 p.setColor(mCalendarHourBackground);
1335 r.top = mBannerPlusMargin;
1336 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1338 r.right = mHoursWidth;
1339 canvas.drawRect(r, p);
1342 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
1343 // Draw the horizontal day background banner
1344 p.setColor(mCalendarDateBannerBackground);
1346 r.bottom = mBannerPlusMargin;
1348 r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
1349 canvas.drawRect(r, p);
1351 // Fill the extra space on the right side with the default background
1353 r.right = mViewWidth;
1354 p.setColor(mCalendarGridAreaBackground);
1355 canvas.drawRect(r, p);
1357 // Draw a highlight on the selected day (if any), but only if we are
1358 // displaying more than one day.
1359 if (mSelectionMode != SELECTION_HIDDEN) {
1361 p.setColor(mCalendarDateSelected);
1363 r.bottom = mBannerPlusMargin;
1364 int daynum = mSelectionDay - mFirstJulianDay;
1365 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1366 r.right = r.left + mCellWidth;
1367 canvas.drawRect(r, p);
1371 p.setTextSize(NORMAL_FONT_SIZE);
1372 p.setTextAlign(Paint.Align.CENTER);
1373 int x = mHoursWidth;
1374 int deltaX = mCellWidth + DAY_GAP;
1375 int cell = mFirstJulianDay;
1378 if (mDateStrWidth < mCellWidth) {
1379 dayNames = mDayStrs;
1381 dayNames = mDayStrs2Letter;
1384 p.setTypeface(mBold);
1385 p.setAntiAlias(true);
1386 for (int day = 0; day < mNumDays; day++, cell++) {
1387 drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
1392 private void drawAmPm(Canvas canvas, Paint p) {
1393 p.setColor(mCalendarAmPmLabel);
1394 p.setTextSize(AMPM_FONT_SIZE);
1395 p.setTypeface(mBold);
1396 p.setAntiAlias(true);
1397 mPaint.setTextAlign(Paint.Align.RIGHT);
1398 String text = mAmString;
1399 if (mFirstHour >= 12) {
1402 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
1403 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1404 canvas.drawText(text, right, y, p);
1406 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
1407 // Also draw the "PM"
1409 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
1410 + 2 * mHoursTextHeight + HOUR_GAP;
1411 canvas.drawText(text, right, y, p);
1415 private void drawCurrentTimeMarker(int top, Canvas canvas, Paint p) {
1416 top -= CURRENT_TIME_MARKER_HEIGHT / 2;
1417 p.setColor(mCurrentTimeMarkerColor);
1418 Paint.Style oldStyle = p.getStyle();
1419 p.setStyle(Paint.Style.STROKE);
1420 p.setStrokeWidth(2.0f);
1421 Path mCurrentTimeMarker = mPath;
1422 mCurrentTimeMarker.reset();
1423 mCurrentTimeMarker.moveTo(0, top);
1424 mCurrentTimeMarker.lineTo(0, CURRENT_TIME_MARKER_HEIGHT + top);
1425 mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_INNER_WIDTH, CURRENT_TIME_MARKER_HEIGHT + top);
1426 mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_WIDTH, CURRENT_TIME_MARKER_HEIGHT / 2 + top);
1427 mCurrentTimeMarker.lineTo(CURRENT_TIME_MARKER_INNER_WIDTH, top);
1428 mCurrentTimeMarker.lineTo(0, top);
1429 canvas.drawPath(mCurrentTimeMarker, p);
1430 p.setStyle(oldStyle);
1433 private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) {
1434 //Do a white outline so it'll show up on a red event
1435 p.setColor(mCurrentTimeMarkerBorderColor);
1436 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH;
1437 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2 + CURRENT_TIME_LINE_BORDER_WIDTH;
1438 r.left = left + CURRENT_TIME_LINE_SIDE_BUFFER;
1439 r.right = r.left + mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER;
1440 canvas.drawRect(r, p);
1441 //Then draw the red line
1442 p.setColor(mCurrentTimeMarkerColor);
1443 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
1444 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
1445 canvas.drawRect(r, p);
1448 private void doDraw(Canvas canvas) {
1451 int lineY = mCurrentTime.hour*(mCellHeight + HOUR_GAP)
1452 + ((mCurrentTime.minute * mCellHeight) / 60)
1455 drawGridBackground(r, canvas, p);
1456 drawHours(r, canvas, p);
1459 int x = mHoursWidth;
1460 int deltaX = mCellWidth + DAY_GAP;
1461 int cell = mFirstJulianDay;
1462 for (int day = 0; day < mNumDays; day++, cell++) {
1463 drawEvents(cell, x, HOUR_GAP, canvas, p);
1465 if(cell == mTodayJulianDay) {
1466 //And the current time shows up somewhere on the screen
1467 if(lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
1468 //draw both the marker and the line
1469 drawCurrentTimeMarker(lineY, canvas, p);
1470 drawCurrentTimeLine(r, x, lineY, canvas, p);
1477 private void drawHours(Rect r, Canvas canvas, Paint p) {
1478 // Draw the background for the hour labels
1479 p.setColor(mCalendarHourBackground);
1481 r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
1483 r.right = mHoursWidth;
1484 canvas.drawRect(r, p);
1486 // Fill the bottom left corner with the default grid background
1488 r.bottom = mBitmapHeight;
1489 p.setColor(mCalendarGridAreaBackground);
1490 canvas.drawRect(r, p);
1492 // Draw a highlight on the selected hour (if needed)
1493 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
1494 p.setColor(mCalendarHourSelected);
1495 r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1496 r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
1498 r.right = mHoursWidth;
1499 canvas.drawRect(r, p);
1501 // Also draw the highlight on the grid
1502 p.setColor(mCalendarGridAreaSelected);
1503 int daynum = mSelectionDay - mFirstJulianDay;
1504 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1505 r.right = r.left + mCellWidth;
1506 canvas.drawRect(r, p);
1508 // Draw a border around the highlighted grid hour.
1511 r.bottom -= HOUR_GAP;
1513 path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
1514 canvas.drawPath(path, mSelectionPaint);
1515 saveSelectionPosition(r.left, r.top, r.right, r.bottom);
1518 p.setColor(mCalendarHourLabel);
1519 p.setTextSize(HOURS_FONT_SIZE);
1520 p.setTypeface(mBold);
1521 p.setTextAlign(Paint.Align.RIGHT);
1522 p.setAntiAlias(true);
1524 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1525 int y = HOUR_GAP + mHoursTextHeight;
1527 for (int i = 0; i < 24; i++) {
1528 String time = mHourStrs[i];
1529 canvas.drawText(time, right, y, p);
1530 y += mCellHeight + HOUR_GAP;
1534 private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
1535 float xCenter = x + mCellWidth / 2.0f;
1537 if (Utils.isSaturday(day, mStartDay)) {
1538 p.setColor(mWeek_saturdayColor);
1539 } else if (Utils.isSunday(day, mStartDay)) {
1540 p.setColor(mWeek_sundayColor);
1542 p.setColor(mCalendarDateBannerTextColor);
1545 int dateNum = mFirstDate + day;
1546 if (dateNum > mMonthLength) {
1547 dateNum -= mMonthLength;
1551 // Add a leading zero if the date is a single digit
1553 dateNumStr = "0" + dateNum;
1555 dateNumStr = String.valueOf(dateNum);
1558 DayHeader header = dayHeaders[day];
1559 if (header == null || header.cell != cell) {
1560 // The day header string is regenerated on every draw during drag and fling animation.
1561 // Caching day header since formatting the string takes surprising long time.
1563 dayHeaders[day] = new DayHeader();
1564 dayHeaders[day].cell = cell;
1565 dayHeaders[day].dateString = getResources().getString(
1566 R.string.weekday_day, dateStr, dateNumStr);
1568 dateStr = dayHeaders[day].dateString;
1570 float y = mBannerPlusMargin - 7;
1571 canvas.drawText(dateStr, xCenter, y, p);
1574 private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
1575 Paint.Style savedStyle = p.getStyle();
1577 // Clear the background
1578 p.setColor(mCalendarGridAreaBackground);
1580 r.bottom = mBitmapHeight;
1582 r.right = mViewWidth;
1583 canvas.drawRect(r, p);
1585 // Draw the horizontal grid lines
1586 p.setColor(mCalendarGridLineHorizontalColor);
1587 p.setStyle(Style.STROKE);
1588 p.setStrokeWidth(0);
1589 p.setAntiAlias(false);
1590 float startX = mHoursWidth;
1591 float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
1593 float deltaY = mCellHeight + HOUR_GAP;
1594 for (int hour = 0; hour <= 24; hour++) {
1595 canvas.drawLine(startX, y, stopX, y, p);
1599 // Draw the vertical grid lines
1600 p.setColor(mCalendarGridLineVerticalColor);
1602 float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
1603 float deltaX = mCellWidth + DAY_GAP;
1604 float x = mHoursWidth + mCellWidth;
1605 for (int day = 0; day < mNumDays; day++) {
1606 canvas.drawLine(x, startY, x, stopY, p);
1610 // Restore the saved style.
1611 p.setStyle(savedStyle);
1612 p.setAntiAlias(true);
1615 Event getSelectedEvent() {
1616 if (mSelectedEvent == null) {
1617 // There is no event at the selected hour, so create a new event.
1618 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1619 getSelectedMinutesSinceMidnight());
1621 return mSelectedEvent;
1624 boolean isEventSelected() {
1625 return (mSelectedEvent != null);
1628 Event getNewEvent() {
1629 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1630 getSelectedMinutesSinceMidnight());
1633 static Event getNewEvent(int julianDay, long utcMillis,
1634 int minutesSinceMidnight) {
1635 Event event = Event.newInstance();
1636 event.startDay = julianDay;
1637 event.endDay = julianDay;
1638 event.startMillis = utcMillis;
1639 event.endMillis = event.startMillis + MILLIS_PER_HOUR;
1640 event.startTime = minutesSinceMidnight;
1641 event.endTime = event.startTime + MINUTES_PER_HOUR;
1645 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
1646 float maxWidthF = 0.0f;
1648 int len = strings.length;
1649 for (int i = 0; i < len; i++) {
1650 float width = p.measureText(strings[i]);
1651 maxWidthF = Math.max(width, maxWidthF);
1653 int maxWidth = (int) (maxWidthF + 0.5);
1654 if (maxWidth < currentMax) {
1655 maxWidth = currentMax;
1660 private void saveSelectionPosition(float left, float top, float right, float bottom) {
1661 mPrevBox.left = (int) left;
1662 mPrevBox.right = (int) right;
1663 mPrevBox.top = (int) top;
1664 mPrevBox.bottom = (int) bottom;
1667 private Rect getCurrentSelectionPosition() {
1668 Rect box = new Rect();
1669 box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1670 box.bottom = box.top + mCellHeight + HOUR_GAP;
1671 int daynum = mSelectionDay - mFirstJulianDay;
1672 box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1673 box.right = box.left + mCellWidth + DAY_GAP;
1677 private void drawAllDayEvents(int firstDay, int numDays,
1678 Rect r, Canvas canvas, Paint p) {
1679 p.setTextSize(NORMAL_FONT_SIZE);
1680 p.setTextAlign(Paint.Align.LEFT);
1681 Paint eventTextPaint = mEventTextPaint;
1683 // Draw the background for the all-day events area
1684 r.top = mBannerPlusMargin;
1685 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1686 r.left = mHoursWidth;
1687 r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
1688 p.setColor(mCalendarAllDayBackground);
1689 canvas.drawRect(r, p);
1691 // Fill the extra space on the right side with the default background
1693 r.right = mViewWidth;
1694 p.setColor(mCalendarGridAreaBackground);
1695 canvas.drawRect(r, p);
1697 // Draw the vertical grid lines
1698 p.setColor(mCalendarGridLineVerticalColor);
1699 p.setStyle(Style.STROKE);
1700 p.setStrokeWidth(0);
1701 p.setAntiAlias(false);
1702 float startY = r.top;
1703 float stopY = r.bottom;
1704 float deltaX = mCellWidth + DAY_GAP;
1705 float x = mHoursWidth + mCellWidth;
1706 for (int day = 0; day <= mNumDays; day++) {
1707 canvas.drawLine(x, startY, x, stopY, p);
1710 p.setAntiAlias(true);
1711 p.setStyle(Style.FILL);
1713 int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
1714 float left = mHoursWidth;
1715 int lastDay = firstDay + numDays - 1;
1716 ArrayList<Event> events = mEvents;
1717 int numEvents = events.size();
1718 float drawHeight = mAllDayHeight;
1719 float numRectangles = mMaxAllDayEvents;
1720 for (int i = 0; i < numEvents; i++) {
1721 Event event = events.get(i);
1724 int startDay = event.startDay;
1725 int endDay = event.endDay;
1726 if (startDay > lastDay || endDay < firstDay)
1728 if (startDay < firstDay)
1729 startDay = firstDay;
1730 if (endDay > lastDay)
1732 int startIndex = startDay - firstDay;
1733 int endIndex = endDay - firstDay;
1734 float height = drawHeight / numRectangles;
1736 // Prevent a single event from getting too big
1737 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
1738 height = MAX_ALLDAY_EVENT_HEIGHT;
1741 // Leave a one-pixel space between the vertical day lines and the
1743 event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
1744 event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
1745 event.top = y + height * event.getColumn();
1747 // Multiply the height by 0.9 to leave a little gap between events
1748 event.bottom = event.top + height * 0.9f;
1750 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1751 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1753 // Check if this all-day event intersects the selected day
1754 if (mSelectionAllDay && mComputeSelectedEvents) {
1755 if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
1756 mSelectedEvents.add(event);
1761 if (mSelectionAllDay) {
1762 // Compute the neighbors for the list of all-day events that
1763 // intersect the selected day.
1764 computeAllDayNeighbors();
1765 if (mSelectedEvent != null) {
1766 Event event = mSelectedEvent;
1767 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1768 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1771 // Draw the highlight on the selected all-day area
1772 float top = mBannerPlusMargin + 1;
1773 float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
1774 int daynum = mSelectionDay - mFirstJulianDay;
1775 left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
1776 float right = left + mCellWidth + DAY_GAP - 1;
1777 if (mNumDays == 1) {
1778 // The Day view doesn't have a vertical line on the right.
1783 path.addRect(left, top, right, bottom, Direction.CW);
1784 canvas.drawPath(path, mSelectionPaint);
1786 // Set the selection position to zero so that when we move down
1787 // to the normal event area, we will highlight the topmost event.
1788 saveSelectionPosition(0f, 0f, 0f, 0f);
1792 private void computeAllDayNeighbors() {
1793 int len = mSelectedEvents.size();
1794 if (len == 0 || mSelectedEvent != null) {
1798 // First, clear all the links
1799 for (int ii = 0; ii < len; ii++) {
1800 Event ev = mSelectedEvents.get(ii);
1804 ev.nextRight = null;
1807 // For each event in the selected event list "mSelectedEvents", find
1808 // its neighbors in the up and down directions. This could be done
1809 // more efficiently by sorting on the Event.getColumn() field, but
1810 // the list is expected to be very small.
1812 // Find the event in the same row as the previously selected all-day
1814 int startPosition = -1;
1815 if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
1816 startPosition = mPrevSelectedEvent.getColumn();
1818 int maxPosition = -1;
1819 Event startEvent = null;
1820 Event maxPositionEvent = null;
1821 for (int ii = 0; ii < len; ii++) {
1822 Event ev = mSelectedEvents.get(ii);
1823 int position = ev.getColumn();
1824 if (position == startPosition) {
1826 } else if (position > maxPosition) {
1827 maxPositionEvent = ev;
1828 maxPosition = position;
1830 for (int jj = 0; jj < len; jj++) {
1834 Event neighbor = mSelectedEvents.get(jj);
1835 int neighborPosition = neighbor.getColumn();
1836 if (neighborPosition == position - 1) {
1837 ev.nextUp = neighbor;
1838 } else if (neighborPosition == position + 1) {
1839 ev.nextDown = neighbor;
1843 if (startEvent != null) {
1844 mSelectedEvent = startEvent;
1846 mSelectedEvent = maxPositionEvent;
1850 RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
1851 // If this event is selected, then use the selection color
1852 if (mSelectedEvent == event) {
1853 // Also, remember the last selected event that we drew
1854 mPrevSelectedEvent = event;
1855 p.setColor(mSelectionColor);
1856 eventTextPaint.setColor(mSelectedEventTextColor);
1858 // Use the normal color for all-day events
1859 p.setColor(event.color);
1860 eventTextPaint.setColor(mEventTextColor);
1865 rf.bottom = event.bottom;
1866 rf.left = event.left;
1867 rf.right = event.right;
1868 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
1875 private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
1876 Paint eventTextPaint = mEventTextPaint;
1877 int cellWidth = mCellWidth;
1878 int cellHeight = mCellHeight;
1880 // Use the selected hour as the selection region
1881 Rect selectionArea = mRect;
1882 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
1883 selectionArea.bottom = selectionArea.top + cellHeight;
1884 selectionArea.left = left;
1885 selectionArea.right = selectionArea.left + cellWidth;
1887 ArrayList<Event> events = mEvents;
1888 int numEvents = events.size();
1889 EventGeometry geometry = mEventGeometry;
1891 for (int i = 0; i < numEvents; i++) {
1892 Event event = events.get(i);
1893 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
1897 if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
1898 && geometry.eventIntersectsSelection(event, selectionArea)) {
1899 mSelectedEvents.add(event);
1902 RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
1903 drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1906 if (date == mSelectionDay && !mSelectionAllDay && isFocused()
1907 && mSelectionMode != SELECTION_HIDDEN) {
1909 if (mSelectedEvent != null) {
1910 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
1911 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
1916 // Computes the "nearest" neighbor event in four directions (left, right,
1917 // up, down) for each of the events in the mSelectedEvents array.
1918 private void computeNeighbors() {
1919 int len = mSelectedEvents.size();
1920 if (len == 0 || mSelectedEvent != null) {
1924 // First, clear all the links
1925 for (int ii = 0; ii < len; ii++) {
1926 Event ev = mSelectedEvents.get(ii);
1930 ev.nextRight = null;
1933 Event startEvent = mSelectedEvents.get(0);
1934 int startEventDistance1 = 100000; // any large number
1935 int startEventDistance2 = 100000; // any large number
1936 int prevLocation = FROM_NONE;
1942 Rect box = getCurrentSelectionPosition();
1943 if (mPrevSelectedEvent != null) {
1944 prevTop = (int) mPrevSelectedEvent.top;
1945 prevBottom = (int) mPrevSelectedEvent.bottom;
1946 prevLeft = (int) mPrevSelectedEvent.left;
1947 prevRight = (int) mPrevSelectedEvent.right;
1948 // Check if the previously selected event intersects the previous
1949 // selection box. (The previously selected event may be from a
1950 // much older selection box.)
1951 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
1952 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
1953 mPrevSelectedEvent = null;
1954 prevTop = mPrevBox.top;
1955 prevBottom = mPrevBox.bottom;
1956 prevLeft = mPrevBox.left;
1957 prevRight = mPrevBox.right;
1959 // Clip the top and bottom to the previous selection box.
1960 if (prevTop < mPrevBox.top) {
1961 prevTop = mPrevBox.top;
1963 if (prevBottom > mPrevBox.bottom) {
1964 prevBottom = mPrevBox.bottom;
1968 // Just use the previously drawn selection box
1969 prevTop = mPrevBox.top;
1970 prevBottom = mPrevBox.bottom;
1971 prevLeft = mPrevBox.left;
1972 prevRight = mPrevBox.right;
1975 // Figure out where we came from and compute the center of that area.
1976 if (prevLeft >= box.right) {
1977 // The previously selected event was to the right of us.
1978 prevLocation = FROM_RIGHT;
1979 prevCenter = (prevTop + prevBottom) / 2;
1980 } else if (prevRight <= box.left) {
1981 // The previously selected event was to the left of us.
1982 prevLocation = FROM_LEFT;
1983 prevCenter = (prevTop + prevBottom) / 2;
1984 } else if (prevBottom <= box.top) {
1985 // The previously selected event was above us.
1986 prevLocation = FROM_ABOVE;
1987 prevCenter = (prevLeft + prevRight) / 2;
1988 } else if (prevTop >= box.bottom) {
1989 // The previously selected event was below us.
1990 prevLocation = FROM_BELOW;
1991 prevCenter = (prevLeft + prevRight) / 2;
1994 // For each event in the selected event list "mSelectedEvents", search
1995 // all the other events in that list for the nearest neighbor in 4
1997 for (int ii = 0; ii < len; ii++) {
1998 Event ev = mSelectedEvents.get(ii);
2000 int startTime = ev.startTime;
2001 int endTime = ev.endTime;
2002 int left = (int) ev.left;
2003 int right = (int) ev.right;
2004 int top = (int) ev.top;
2005 if (top < box.top) {
2008 int bottom = (int) ev.bottom;
2009 if (bottom > box.bottom) {
2010 bottom = box.bottom;
2013 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2014 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2015 if (DateFormat.is24HourFormat(mParentActivity)) {
2016 flags |= DateUtils.FORMAT_24HOUR;
2018 String timeRange = DateUtils.formatDateRange(mParentActivity,
2019 ev.startMillis, ev.endMillis, flags);
2020 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
2021 + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
2023 int upDistanceMin = 10000; // any large number
2024 int downDistanceMin = 10000; // any large number
2025 int leftDistanceMin = 10000; // any large number
2026 int rightDistanceMin = 10000; // any large number
2027 Event upEvent = null;
2028 Event downEvent = null;
2029 Event leftEvent = null;
2030 Event rightEvent = null;
2032 // Pick the starting event closest to the previously selected event,
2033 // if any. distance1 takes precedence over distance2.
2036 if (prevLocation == FROM_ABOVE) {
2037 if (left >= prevCenter) {
2038 distance1 = left - prevCenter;
2039 } else if (right <= prevCenter) {
2040 distance1 = prevCenter - right;
2042 distance2 = top - prevBottom;
2043 } else if (prevLocation == FROM_BELOW) {
2044 if (left >= prevCenter) {
2045 distance1 = left - prevCenter;
2046 } else if (right <= prevCenter) {
2047 distance1 = prevCenter - right;
2049 distance2 = prevTop - bottom;
2050 } else if (prevLocation == FROM_LEFT) {
2051 if (bottom <= prevCenter) {
2052 distance1 = prevCenter - bottom;
2053 } else if (top >= prevCenter) {
2054 distance1 = top - prevCenter;
2056 distance2 = left - prevRight;
2057 } else if (prevLocation == FROM_RIGHT) {
2058 if (bottom <= prevCenter) {
2059 distance1 = prevCenter - bottom;
2060 } else if (top >= prevCenter) {
2061 distance1 = top - prevCenter;
2063 distance2 = prevLeft - right;
2065 if (distance1 < startEventDistance1
2066 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
2068 startEventDistance1 = distance1;
2069 startEventDistance2 = distance2;
2072 // For each neighbor, figure out if it is above or below or left
2073 // or right of me and compute the distance.
2074 for (int jj = 0; jj < len; jj++) {
2078 Event neighbor = mSelectedEvents.get(jj);
2079 int neighborLeft = (int) neighbor.left;
2080 int neighborRight = (int) neighbor.right;
2081 if (neighbor.endTime <= startTime) {
2082 // This neighbor is entirely above me.
2083 // If we overlap the same column, then compute the distance.
2084 if (neighborLeft < right && neighborRight > left) {
2085 int distance = startTime - neighbor.endTime;
2086 if (distance < upDistanceMin) {
2087 upDistanceMin = distance;
2089 } else if (distance == upDistanceMin) {
2090 int center = (left + right) / 2;
2091 int currentDistance = 0;
2092 int currentLeft = (int) upEvent.left;
2093 int currentRight = (int) upEvent.right;
2094 if (currentRight <= center) {
2095 currentDistance = center - currentRight;
2096 } else if (currentLeft >= center) {
2097 currentDistance = currentLeft - center;
2100 int neighborDistance = 0;
2101 if (neighborRight <= center) {
2102 neighborDistance = center - neighborRight;
2103 } else if (neighborLeft >= center) {
2104 neighborDistance = neighborLeft - center;
2106 if (neighborDistance < currentDistance) {
2107 upDistanceMin = distance;
2112 } else if (neighbor.startTime >= endTime) {
2113 // This neighbor is entirely below me.
2114 // If we overlap the same column, then compute the distance.
2115 if (neighborLeft < right && neighborRight > left) {
2116 int distance = neighbor.startTime - endTime;
2117 if (distance < downDistanceMin) {
2118 downDistanceMin = distance;
2119 downEvent = neighbor;
2120 } else if (distance == downDistanceMin) {
2121 int center = (left + right) / 2;
2122 int currentDistance = 0;
2123 int currentLeft = (int) downEvent.left;
2124 int currentRight = (int) downEvent.right;
2125 if (currentRight <= center) {
2126 currentDistance = center - currentRight;
2127 } else if (currentLeft >= center) {
2128 currentDistance = currentLeft - center;
2131 int neighborDistance = 0;
2132 if (neighborRight <= center) {
2133 neighborDistance = center - neighborRight;
2134 } else if (neighborLeft >= center) {
2135 neighborDistance = neighborLeft - center;
2137 if (neighborDistance < currentDistance) {
2138 downDistanceMin = distance;
2139 downEvent = neighbor;
2145 if (neighborLeft >= right) {
2146 // This neighbor is entirely to the right of me.
2147 // Take the closest neighbor in the y direction.
2148 int center = (top + bottom) / 2;
2150 int neighborBottom = (int) neighbor.bottom;
2151 int neighborTop = (int) neighbor.top;
2152 if (neighborBottom <= center) {
2153 distance = center - neighborBottom;
2154 } else if (neighborTop >= center) {
2155 distance = neighborTop - center;
2157 if (distance < rightDistanceMin) {
2158 rightDistanceMin = distance;
2159 rightEvent = neighbor;
2160 } else if (distance == rightDistanceMin) {
2161 // Pick the closest in the x direction
2162 int neighborDistance = neighborLeft - right;
2163 int currentDistance = (int) rightEvent.left - right;
2164 if (neighborDistance < currentDistance) {
2165 rightDistanceMin = distance;
2166 rightEvent = neighbor;
2169 } else if (neighborRight <= left) {
2170 // This neighbor is entirely to the left of me.
2171 // Take the closest neighbor in the y direction.
2172 int center = (top + bottom) / 2;
2174 int neighborBottom = (int) neighbor.bottom;
2175 int neighborTop = (int) neighbor.top;
2176 if (neighborBottom <= center) {
2177 distance = center - neighborBottom;
2178 } else if (neighborTop >= center) {
2179 distance = neighborTop - center;
2181 if (distance < leftDistanceMin) {
2182 leftDistanceMin = distance;
2183 leftEvent = neighbor;
2184 } else if (distance == leftDistanceMin) {
2185 // Pick the closest in the x direction
2186 int neighborDistance = left - neighborRight;
2187 int currentDistance = left - (int) leftEvent.right;
2188 if (neighborDistance < currentDistance) {
2189 leftDistanceMin = distance;
2190 leftEvent = neighbor;
2195 ev.nextUp = upEvent;
2196 ev.nextDown = downEvent;
2197 ev.nextLeft = leftEvent;
2198 ev.nextRight = rightEvent;
2200 mSelectedEvent = startEvent;
2204 private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
2206 int color = event.color;
2208 // Fade visible boxes if event was declined.
2209 boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
2211 int alpha = color & 0xff000000;
2212 color &= 0x00ffffff;
2213 int red = (color & 0x00ff0000) >> 16;
2214 int green = (color & 0x0000ff00) >> 8;
2215 int blue = (color & 0x0000ff);
2216 color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
2217 color += 0x7F7F7F + alpha;
2220 // If this event is selected, then use the selection color
2221 if (mSelectedEvent == event) {
2222 if (mSelectionMode == SELECTION_PRESSED) {
2223 // Also, remember the last selected event that we drew
2224 mPrevSelectedEvent = event;
2225 // box = mBoxPressed;
2226 p.setColor(mPressedColor); // FIXME:pressed
2227 eventTextPaint.setColor(mSelectedEventTextColor);
2228 } else if (mSelectionMode == SELECTION_SELECTED) {
2229 // Also, remember the last selected event that we drew
2230 mPrevSelectedEvent = event;
2231 // box = mBoxSelected;
2232 p.setColor(mSelectionColor);
2233 eventTextPaint.setColor(mSelectedEventTextColor);
2234 } else if (mSelectionMode == SELECTION_LONGPRESS) {
2235 // box = mBoxLongPressed;
2236 p.setColor(mPressedColor); // FIXME: longpressed (maybe -- this doesn't seem to work)
2237 eventTextPaint.setColor(mSelectedEventTextColor);
2240 eventTextPaint.setColor(mEventTextColor);
2244 eventTextPaint.setColor(mEventTextColor);
2250 rf.bottom = event.bottom;
2251 rf.left = event.left;
2252 rf.right = event.right - 1;
2254 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
2256 // Draw a darker border
2257 float[] hsv = new float[3];
2258 Color.colorToHSV(p.getColor(), hsv);
2261 mPaintBorder.setColor(Color.HSVToColor(hsv));
2262 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
2270 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
2272 // Sanitize a string before passing it to drawText or else we get little
2273 // squares. For newlines and tabs before a comma, delete the character.
2274 // Otherwise, just replace them with a space.
2275 private String drawTextSanitizer(String string) {
2276 Matcher m = drawTextSanitizerFilter.matcher(string);
2277 string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
2281 private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
2282 if (!mDrawTextInEventRect) {
2286 float width = rf.right - rf.left;
2287 float height = rf.bottom - rf.top;
2289 // Leave one pixel extra space between lines
2290 int lineHeight = mEventTextHeight + 1;
2292 // If the rectangle is too small for text, then return
2293 if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
2297 // Truncate the event title to a known (large enough) limit
2298 String text = event.getTitleAndLocation();
2300 text = drawTextSanitizer(text);
2302 int len = text.length();
2303 if (len > MAX_EVENT_TEXT_LEN) {
2304 text = text.substring(0, MAX_EVENT_TEXT_LEN);
2305 len = MAX_EVENT_TEXT_LEN;
2308 // Figure out how much space the event title will take, and create a
2309 // String fragment that will fit in the rectangle. Use multiple lines,
2311 p.getTextWidths(text, mCharWidths);
2312 String fragment = text;
2313 float top = rf.top + mEventTextAscent + topMargin;
2316 // Leave one pixel extra space at the bottom
2317 while (start < len && height >= (lineHeight + 1)) {
2318 boolean lastLine = (height < 2 * lineHeight + 1);
2319 // Skip leading spaces at the beginning of each line
2321 char c = text.charAt(start);
2322 if (c != ' ') break;
2324 } while (start < len);
2328 for (int ii = start; ii < len; ii++) {
2329 char c = text.charAt(ii);
2331 // If we found the end of a word, then remember the ending
2336 sum += mCharWidths[ii];
2337 // If adding this character would exceed the width and this
2338 // isn't the last line, then break the line at the previous
2339 // word. If there was no previous word, then break this word.
2341 if (end > start && !lastLine) {
2342 // There was a previous word on this line.
2343 fragment = text.substring(start, end);
2348 // This is the only word and it is too long to fit on
2349 // the line (or this is the last line), so take as many
2350 // characters of this word as will fit.
2351 fragment = text.substring(start, ii);
2357 // If sum <= width, then we can fit the rest of the text on
2360 fragment = text.substring(start, len);
2364 canvas.drawText(fragment, rf.left + 1, top, p);
2367 height -= lineHeight;
2371 private void updateEventDetails() {
2372 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
2373 || mSelectionMode == SELECTION_LONGPRESS) {
2377 if (mLastPopupEventID == mSelectedEvent.id) {
2381 mLastPopupEventID = mSelectedEvent.id;
2383 // Remove any outstanding callbacks to dismiss the popup.
2384 getHandler().removeCallbacks(mDismissPopup);
2386 Event event = mSelectedEvent;
2387 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
2388 titleView.setText(event.title);
2390 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
2391 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
2393 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
2394 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
2398 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
2399 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
2401 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
2402 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
2403 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2405 if (DateFormat.is24HourFormat(mParentActivity)) {
2406 flags |= DateUtils.FORMAT_24HOUR;
2408 String timeRange = DateUtils.formatDateRange(mParentActivity,
2409 event.startMillis, event.endMillis, flags);
2410 TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
2411 timeView.setText(timeRange);
2413 TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
2414 final boolean empty = TextUtils.isEmpty(event.location);
2415 whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
2416 if (!empty) whereView.setText(event.location);
2418 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
2419 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
2422 // The following routines are called from the parent activity when certain
2423 // touch events occur.
2425 void doDown(MotionEvent ev) {
2426 mTouchMode = TOUCH_MODE_DOWN;
2428 mOnFlingCalled = false;
2429 getHandler().removeCallbacks(mContinueScroll);
2432 void doSingleTapUp(MotionEvent ev) {
2433 int x = (int) ev.getX();
2434 int y = (int) ev.getY();
2435 Event selectedEvent = mSelectedEvent;
2436 int selectedDay = mSelectionDay;
2437 int selectedHour = mSelectionHour;
2439 boolean validPosition = setSelectionFromPosition(x, y);
2440 if (!validPosition) {
2441 // return if the touch wasn't on an area of concern
2445 mSelectionMode = SELECTION_SELECTED;
2446 mRedrawScreen = true;
2449 boolean launchNewView = false;
2450 if (mSelectedEvent != null) {
2451 // If the tap is on an event, launch the "View event" view
2452 launchNewView = true;
2453 } else if (mSelectedEvent == null && selectedDay == mSelectionDay
2454 && selectedHour == mSelectionHour) {
2455 // If the tap is on an already selected hour slot,
2456 // then launch the Day/Agenda view. Otherwise, just select the hour
2458 launchNewView = true;
2461 if (launchNewView) {
2462 switchViews(false /* not the trackball */);
2466 void doLongPress(MotionEvent ev) {
2467 int x = (int) ev.getX();
2468 int y = (int) ev.getY();
2470 boolean validPosition = setSelectionFromPosition(x, y);
2471 if (!validPosition) {
2472 // return if the touch wasn't on an area of concern
2476 mSelectionMode = SELECTION_LONGPRESS;
2477 mRedrawScreen = true;
2482 void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
2483 // Use the distance from the current point to the initial touch instead
2484 // of deltaX and deltaY to avoid accumulating floating-point rounding
2485 // errors. Also, we don't need floats, we can use ints.
2486 int distanceX = (int) e1.getX() - (int) e2.getX();
2487 int distanceY = (int) e1.getY() - (int) e2.getY();
2489 // If we haven't figured out the predominant scroll direction yet,
2491 if (mTouchMode == TOUCH_MODE_DOWN) {
2492 int absDistanceX = Math.abs(distanceX);
2493 int absDistanceY = Math.abs(distanceY);
2494 mScrollStartY = mViewStartY;
2495 mPreviousDistanceX = 0;
2496 mPreviousDirection = 0;
2498 // If the x distance is at least twice the y distance, then lock
2499 // the scroll horizontally. Otherwise scroll vertically.
2500 if (absDistanceX >= 2 * absDistanceY) {
2501 mTouchMode = TOUCH_MODE_HSCROLL;
2502 mViewStartX = distanceX;
2503 initNextView(-mViewStartX);
2505 mTouchMode = TOUCH_MODE_VSCROLL;
2507 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2508 // We are already scrolling horizontally, so check if we
2509 // changed the direction of scrolling so that the other week
2511 mViewStartX = distanceX;
2512 if (distanceX != 0) {
2513 int direction = (distanceX > 0) ? 1 : -1;
2514 if (direction != mPreviousDirection) {
2515 // The user has switched the direction of scrolling
2516 // so re-init the next view
2517 initNextView(-mViewStartX);
2518 mPreviousDirection = direction;
2522 // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
2523 // then change the title to the new day (or week), but only
2524 // if we haven't already changed the title.
2525 if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
2526 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
2527 CalendarView view = mParentActivity.getNextView();
2528 mTitleTextView.setText(view.mDateRange);
2530 } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2531 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
2532 CalendarView view = mParentActivity.getNextView();
2533 mTitleTextView.setText(view.mDateRange);
2536 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
2537 || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2538 mTitleTextView.setText(mDateRange);
2541 mPreviousDistanceX = distanceX;
2544 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
2545 mViewStartY = mScrollStartY + distanceY;
2546 if (mViewStartY < 0) {
2548 } else if (mViewStartY > mMaxViewStartY) {
2549 mViewStartY = mMaxViewStartY;
2556 if (mSelectionMode != SELECTION_HIDDEN) {
2557 mSelectionMode = SELECTION_HIDDEN;
2558 mRedrawScreen = true;
2563 void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2564 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2565 mSelectionMode = SELECTION_HIDDEN;
2566 mOnFlingCalled = true;
2567 int deltaX = (int) e2.getX() - (int) e1.getX();
2568 int distanceX = Math.abs(deltaX);
2569 int deltaY = (int) e2.getY() - (int) e1.getY();
2570 int distanceY = Math.abs(deltaY);
2572 if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
2573 boolean switchForward = initNextView(deltaX);
2574 CalendarView view = mParentActivity.getNextView();
2575 mTitleTextView.setText(view.mDateRange);
2576 mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
2581 // Continue scrolling vertically
2582 mContinueScroll.init((int) velocityY / 20);
2583 post(mContinueScroll);
2586 private boolean initNextView(int deltaX) {
2587 // Change the view to the previous day or week
2588 CalendarView view = mParentActivity.getNextView();
2589 Time date = view.mBaseDate;
2590 date.set(mBaseDate);
2591 boolean switchForward;
2593 date.monthDay -= mNumDays;
2594 view.mSelectionDay = mSelectionDay - mNumDays;
2595 switchForward = false;
2597 date.monthDay += mNumDays;
2598 view.mSelectionDay = mSelectionDay + mNumDays;
2599 switchForward = true;
2601 date.normalize(true /* ignore isDst */);
2603 view.layout(getLeft(), getTop(), getRight(), getBottom());
2604 view.reloadEvents();
2605 return switchForward;
2609 public boolean onTouchEvent(MotionEvent ev) {
2610 int action = ev.getAction();
2613 case MotionEvent.ACTION_DOWN:
2614 mParentActivity.mGestureDetector.onTouchEvent(ev);
2617 case MotionEvent.ACTION_MOVE:
2618 mParentActivity.mGestureDetector.onTouchEvent(ev);
2621 case MotionEvent.ACTION_UP:
2622 mParentActivity.mGestureDetector.onTouchEvent(ev);
2623 if (mOnFlingCalled) {
2626 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2627 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2628 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
2629 // The user has gone beyond the threshold so switch views
2630 mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
2634 // Not beyond the threshold so invalidate which will cause
2635 // the view to snap back. Also call recalc() to ensure
2636 // that we have the correct starting date and title.
2638 mTitleTextView.setText(mDateRange);
2644 // If we were scrolling, then reset the selected hour so that it
2648 resetSelectedHour();
2649 mRedrawScreen = true;
2654 // This case isn't expected to happen.
2655 case MotionEvent.ACTION_CANCEL:
2656 mParentActivity.mGestureDetector.onTouchEvent(ev);
2658 resetSelectedHour();
2662 if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
2665 return super.onTouchEvent(ev);
2669 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
2672 // If the trackball is held down, then the context menu pops up and
2673 // we never get onKeyUp() for the long-press. So check for it here
2674 // and change the selection to the long-press state.
2675 if (mSelectionMode != SELECTION_LONGPRESS) {
2676 mSelectionMode = SELECTION_LONGPRESS;
2677 mRedrawScreen = true;
2681 final long startMillis = getSelectedTimeInMillis();
2682 int flags = DateUtils.FORMAT_SHOW_TIME
2683 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
2684 | DateUtils.FORMAT_SHOW_WEEKDAY;
2685 final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
2686 menu.setHeaderTitle(title);
2688 int numSelectedEvents = mSelectedEvents.size();
2689 if (mNumDays == 1) {
2692 // If there is a selected event, then allow it to be viewed and
2694 if (numSelectedEvents >= 1) {
2695 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2696 item.setOnMenuItemClickListener(mContextMenuHandler);
2697 item.setIcon(android.R.drawable.ic_menu_info_details);
2699 if (isEventEditable(mParentActivity, mSelectedEvent)) {
2700 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2701 item.setOnMenuItemClickListener(mContextMenuHandler);
2702 item.setIcon(android.R.drawable.ic_menu_edit);
2703 item.setAlphabeticShortcut('e');
2705 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2706 item.setOnMenuItemClickListener(mContextMenuHandler);
2707 item.setIcon(android.R.drawable.ic_menu_delete);
2710 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2711 item.setOnMenuItemClickListener(mContextMenuHandler);
2712 item.setIcon(android.R.drawable.ic_menu_add);
2713 item.setAlphabeticShortcut('n');
2715 // Otherwise, if the user long-pressed on a blank hour, allow
2716 // them to create an event. They can also do this by tapping.
2717 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2718 item.setOnMenuItemClickListener(mContextMenuHandler);
2719 item.setIcon(android.R.drawable.ic_menu_add);
2720 item.setAlphabeticShortcut('n');
2725 // If there is a selected event, then allow it to be viewed and
2727 if (numSelectedEvents >= 1) {
2728 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2729 item.setOnMenuItemClickListener(mContextMenuHandler);
2730 item.setIcon(android.R.drawable.ic_menu_info_details);
2732 if (isEventEditable(mParentActivity, mSelectedEvent)) {
2733 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2734 item.setOnMenuItemClickListener(mContextMenuHandler);
2735 item.setIcon(android.R.drawable.ic_menu_edit);
2736 item.setAlphabeticShortcut('e');
2738 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2739 item.setOnMenuItemClickListener(mContextMenuHandler);
2740 item.setIcon(android.R.drawable.ic_menu_delete);
2743 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2744 item.setOnMenuItemClickListener(mContextMenuHandler);
2745 item.setIcon(android.R.drawable.ic_menu_add);
2746 item.setAlphabeticShortcut('n');
2748 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2749 item.setOnMenuItemClickListener(mContextMenuHandler);
2750 item.setIcon(android.R.drawable.ic_menu_day);
2751 item.setAlphabeticShortcut('d');
2753 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2754 item.setOnMenuItemClickListener(mContextMenuHandler);
2755 item.setIcon(android.R.drawable.ic_menu_agenda);
2756 item.setAlphabeticShortcut('a');
2758 // No events are selected
2759 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2760 item.setOnMenuItemClickListener(mContextMenuHandler);
2761 item.setIcon(android.R.drawable.ic_menu_add);
2762 item.setAlphabeticShortcut('n');
2764 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2765 item.setOnMenuItemClickListener(mContextMenuHandler);
2766 item.setIcon(android.R.drawable.ic_menu_day);
2767 item.setAlphabeticShortcut('d');
2769 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2770 item.setOnMenuItemClickListener(mContextMenuHandler);
2771 item.setIcon(android.R.drawable.ic_menu_agenda);
2772 item.setAlphabeticShortcut('a');
2779 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
2780 public boolean onMenuItemClick(MenuItem item) {
2781 switch (item.getItemId()) {
2782 case MenuHelper.MENU_EVENT_VIEW: {
2783 if (mSelectedEvent != null) {
2784 long id = mSelectedEvent.id;
2785 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2786 Intent intent = new Intent(Intent.ACTION_VIEW);
2787 intent.setData(eventUri);
2788 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
2789 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2790 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2791 mParentActivity.startActivity(intent);
2795 case MenuHelper.MENU_EVENT_EDIT: {
2796 if (mSelectedEvent != null) {
2797 long id = mSelectedEvent.id;
2798 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
2799 Intent intent = new Intent(Intent.ACTION_EDIT);
2800 intent.setData(eventUri);
2801 intent.setClassName(mParentActivity, EditEvent.class.getName());
2802 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
2803 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
2804 mParentActivity.startActivity(intent);
2808 case MenuHelper.MENU_DAY: {
2809 long startMillis = getSelectedTimeInMillis();
2810 Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
2813 case MenuHelper.MENU_AGENDA: {
2814 long startMillis = getSelectedTimeInMillis();
2815 Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
2818 case MenuHelper.MENU_EVENT_CREATE: {
2819 long startMillis = getSelectedTimeInMillis();
2820 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
2821 Intent intent = new Intent(Intent.ACTION_VIEW);
2822 intent.setClassName(mParentActivity, EditEvent.class.getName());
2823 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
2824 intent.putExtra(EVENT_END_TIME, endMillis);
2825 intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
2826 mParentActivity.startActivity(intent);
2829 case MenuHelper.MENU_EVENT_DELETE: {
2830 if (mSelectedEvent != null) {
2831 Event selectedEvent = mSelectedEvent;
2832 long begin = selectedEvent.startMillis;
2833 long end = selectedEvent.endMillis;
2834 long id = selectedEvent.id;
2835 mDeleteEventHelper.delete(begin, end, id, -1);
2847 private static boolean isEventEditable(Context context, Event e) {
2848 ContentResolver cr = context.getContentResolver();
2850 int visibility = Calendars.NO_ACCESS;
2851 int relationship = Attendees.RELATIONSHIP_ORGANIZER;
2853 // Get the calendar id for this event
2854 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
2855 new String[] { Events.CALENDAR_ID },
2856 null /* selection */,
2857 null /* selectionArgs */,
2860 if (cursor == null) {
2864 if (cursor.getCount() == 0) {
2869 cursor.moveToFirst();
2870 long calId = cursor.getLong(0);
2873 Uri uri = Calendars.CONTENT_URI;
2874 String where = String.format(CALENDARS_WHERE, calId);
2875 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
2877 String calendarOwnerAccount = null;
2878 if (cursor != null) {
2879 cursor.moveToFirst();
2880 visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
2881 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
2885 if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
2889 if (e.guestsCanModify) {
2893 return !TextUtils.isEmpty(calendarOwnerAccount) && calendarOwnerAccount.equals(e.organizer);
2897 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
2898 * If the touch position is not within the displayed grid, then this
2899 * method returns false.
2901 * @param x the x position of the touch
2902 * @param y the y position of the touch
2903 * @return true if the touch position is valid
2905 private boolean setSelectionFromPosition(int x, int y) {
2906 if (x < mHoursWidth) {
2910 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
2911 if (day >= mNumDays) {
2914 day += mFirstJulianDay;
2916 if (y < mFirstCell + mFirstHourOffset) {
2917 mSelectionAllDay = true;
2919 hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
2921 mSelectionHour = hour;
2922 mSelectionAllDay = false;
2924 mSelectionDay = day;
2925 findSelectedEvent(x, y);
2926 // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
2927 // + " hour: " + hour
2928 // + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
2929 // if (mSelectedEvent != null) {
2930 // Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
2931 // for (Event ev : mSelectedEvents) {
2932 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2933 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2934 // String timeRange = formatDateRange(mParentActivity,
2935 // ev.startMillis, ev.endMillis, flags);
2937 // Log.i("Cal", " " + timeRange + " " + ev.title);
2943 private void findSelectedEvent(int x, int y) {
2944 int date = mSelectionDay;
2945 int cellWidth = mCellWidth;
2946 ArrayList<Event> events = mEvents;
2947 int numEvents = events.size();
2948 int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
2950 mSelectedEvent = null;
2952 mSelectedEvents.clear();
2953 if (mSelectionAllDay) {
2955 float minYdistance = 10000.0f; // any large number
2956 Event closestEvent = null;
2957 float drawHeight = mAllDayHeight;
2958 int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
2959 for (int i = 0; i < numEvents; i++) {
2960 Event event = events.get(i);
2961 if (!event.allDay) {
2965 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
2966 float numRectangles = event.getMaxColumns();
2967 float height = drawHeight / numRectangles;
2968 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
2969 height = MAX_ALLDAY_EVENT_HEIGHT;
2971 float eventTop = yOffset + height * event.getColumn();
2972 float eventBottom = eventTop + height;
2973 if (eventTop < y && eventBottom > y) {
2974 // If the touch is inside the event rectangle, then
2976 mSelectedEvents.add(event);
2977 closestEvent = event;
2980 // Find the closest event
2981 if (eventTop >= y) {
2982 yDistance = eventTop - y;
2984 yDistance = y - eventBottom;
2986 if (yDistance < minYdistance) {
2987 minYdistance = yDistance;
2988 closestEvent = event;
2993 mSelectedEvent = closestEvent;
2997 // Adjust y for the scrollable bitmap
2998 y += mViewStartY - mFirstCell;
3000 // Use a region around (x,y) for the selection region
3001 Rect region = mRect;
3002 region.left = x - 10;
3003 region.right = x + 10;
3004 region.top = y - 10;
3005 region.bottom = y + 10;
3007 EventGeometry geometry = mEventGeometry;
3009 for (int i = 0; i < numEvents; i++) {
3010 Event event = events.get(i);
3011 // Compute the event rectangle.
3012 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
3016 // If the event intersects the selection region, then add it to
3018 if (geometry.eventIntersectsSelection(event, region)) {
3019 mSelectedEvents.add(event);
3023 // If there are any events in the selected region, then assign the
3024 // closest one to mSelectedEvent.
3025 if (mSelectedEvents.size() > 0) {
3026 int len = mSelectedEvents.size();
3027 Event closestEvent = null;
3028 float minDist = mViewWidth + mViewHeight; // some large distance
3029 for (int index = 0; index < len; index++) {
3030 Event ev = mSelectedEvents.get(index);
3031 float dist = geometry.pointToEvent(x, y, ev);
3032 if (dist < minDist) {
3037 mSelectedEvent = closestEvent;
3039 // Keep the selected hour and day consistent with the selected
3040 // event. They could be different if we touched on an empty hour
3041 // slot very close to an event in the previous hour slot. In
3042 // that case we will select the nearby event.
3043 int startDay = mSelectedEvent.startDay;
3044 int endDay = mSelectedEvent.endDay;
3045 if (mSelectionDay < startDay) {
3046 mSelectionDay = startDay;
3047 } else if (mSelectionDay > endDay) {
3048 mSelectionDay = endDay;
3051 int startHour = mSelectedEvent.startTime / 60;
3053 if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
3054 endHour = (mSelectedEvent.endTime - 1) / 60;
3056 endHour = mSelectedEvent.endTime / 60;
3059 if (mSelectionHour < startHour) {
3060 mSelectionHour = startHour;
3061 } else if (mSelectionHour > endHour) {
3062 mSelectionHour = endHour;
3067 // Encapsulates the code to continue the scrolling after the
3068 // finger is lifted. Instead of stopping the scroll immediately,
3069 // the scroll continues to "free spin" and gradually slows down.
3070 private class ContinueScroll implements Runnable {
3075 private static final float FRICTION_COEF = 0.7F;
3076 private static final long FREE_SPIN_MILLIS = 180;
3077 private static final int MAX_DELTA = 60;
3078 private static final int SCROLL_REPEAT_INTERVAL = 30;
3080 public void init(int deltaY) {
3084 } else if (deltaY < 0) {
3087 mAbsDeltaY = Math.abs(deltaY);
3089 // Limit the maximum speed
3090 if (mAbsDeltaY > MAX_DELTA) {
3091 mAbsDeltaY = MAX_DELTA;
3093 mFloatDeltaY = mAbsDeltaY;
3094 mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
3095 // Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
3096 // + " mViewStartY: " + mViewStartY);
3100 long time = System.currentTimeMillis();
3102 // Start out with a frictionless "free spin"
3103 if (time > mFreeSpinTime) {
3104 // If the delta is small, then apply a fixed deceleration.
3106 if (mAbsDeltaY <= 10) {
3109 mFloatDeltaY *= FRICTION_COEF;
3110 mAbsDeltaY = (int) mFloatDeltaY;
3113 if (mAbsDeltaY < 0) {
3118 if (mSignDeltaY == 1) {
3119 mViewStartY -= mAbsDeltaY;
3121 mViewStartY += mAbsDeltaY;
3123 // Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
3124 // + " mViewStartY: " + mViewStartY);
3126 if (mViewStartY < 0) {
3129 } else if (mViewStartY > mMaxViewStartY) {
3130 mViewStartY = mMaxViewStartY;
3136 if (mAbsDeltaY > 0) {
3137 postDelayed(this, SCROLL_REPEAT_INTERVAL);
3141 resetSelectedHour();
3142 mRedrawScreen = true;
3150 * Cleanup the pop-up and timers.
3152 public void cleanup() {
3153 // Protect against null-pointer exceptions
3154 if (mPopup != null) {
3157 mLastPopupEventID = INVALID_EVENT_ID;
3158 Handler handler = getHandler();
3159 if (handler != null) {
3160 handler.removeCallbacks(mDismissPopup);
3161 handler.removeCallbacks(mUpdateCurrentTime);
3166 mRedrawScreen = false;
3170 * Restart the update timer
3172 public void restartCurrentTimeUpdates() {
3173 post(mUpdateCurrentTime);
3176 @Override protected void onDetachedFromWindow() {
3178 if (mBitmap != null) {
3182 super.onDetachedFromWindow();
3185 class DismissPopup implements Runnable {
3187 // Protect against null-pointer exceptions
3188 if (mPopup != null) {
3194 class UpdateCurrentTime implements Runnable {
3196 long currentTime = System.currentTimeMillis();
3197 mCurrentTime.set(currentTime);
3198 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
3199 postDelayed(mUpdateCurrentTime,
3200 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
3201 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
3202 mRedrawScreen = true;