summaryrefslogtreecommitdiffstats
path: root/src/com/android/calendar/CalendarView.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calendar/CalendarView.java')
-rw-r--r--src/com/android/calendar/CalendarView.java2999
1 files changed, 2999 insertions, 0 deletions
diff --git a/src/com/android/calendar/CalendarView.java b/src/com/android/calendar/CalendarView.java
new file mode 100644
index 00000000..9126d6eb
--- /dev/null
+++ b/src/com/android/calendar/CalendarView.java
@@ -0,0 +1,2999 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calendar;
+
+import static android.provider.Calendar.EVENT_BEGIN_TIME;
+import static android.provider.Calendar.EVENT_END_TIME;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.Paint.Style;
+import android.graphics.Path.Direction;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.pim.DateFormat;
+import android.pim.DateUtils;
+import android.pim.Time;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+/**
+ * This is the base class for a set of classes that implement views (day view
+ * and week view to start with) that share some common code.
+ */
+public class CalendarView extends View
+ implements View.OnCreateContextMenuListener, View.OnClickListener {
+
+ private boolean mOnFlingCalled;
+
+ protected CalendarApplication mCalendarApp;
+ protected CalendarActivity mParentActivity;
+
+ private static final String[] CALENDARS_PROJECTION = new String[] {
+ Calendars._ID, // 0
+ Calendars.ACCESS_LEVEL, // 1
+ };
+ private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
+ private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
+
+ private static final String[] ATTENDEES_PROJECTION = new String[] {
+ Attendees._ID, // 0
+ Attendees.ATTENDEE_RELATIONSHIP, // 1
+ };
+ private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
+ private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
+
+ private static final float SMALL_ROUND_RADIUS = 3.0F;
+
+ private static final int FROM_NONE = 0;
+ private static final int FROM_ABOVE = 1;
+ private static final int FROM_BELOW = 2;
+ private static final int FROM_LEFT = 4;
+ private static final int FROM_RIGHT = 8;
+
+ private static final int HORIZONTAL_SCROLL_THRESHOLD = 50;
+
+ private ContinueScroll mContinueScroll = new ContinueScroll();
+
+ // Make this visible within the package for more informative debugging
+ Time mBaseDate;
+
+ private Typeface mBold = Typeface.DEFAULT_BOLD;
+ private int mFirstJulianDay;
+ private int mLastJulianDay;
+
+ private int mMonthLength;
+ private int mFirstDate;
+ private int[] mEarliestStartHour; // indexed by the week day offset
+ private boolean[] mHasAllDayEvent; // indexed by the week day offset
+
+ private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
+
+ /**
+ * This variable helps to avoid unnecessarily reloading events by keeping
+ * track of the start millis parameter used for the most recent loading
+ * of events. If the next reload matches this, then the events are not
+ * reloaded. To force a reload, set this to zero (this is set to zero
+ * in the method clearCachedEvents()).
+ */
+ private long mLastReloadMillis;
+
+ private ArrayList<Event> mEvents = new ArrayList<Event>();
+ private Drawable mBoxNormal;
+ private Drawable mBoxSelected;
+ private Drawable mBoxPressed;
+ private Drawable mBoxLongPressed;
+ private int mSelectionDay; // Julian day
+ private int mSelectionHour;
+
+ /* package private so that CalendarActivity can read it when creating new
+ * events
+ */
+ boolean mSelectionAllDay;
+
+ private int mCellWidth;
+ private boolean mLaunchNewView;
+
+ // Pre-allocate these objects and re-use them
+ private Rect mRect = new Rect();
+ private RectF mRectF = new RectF();
+ private Rect mSrcRect = new Rect();
+ private Rect mDestRect = new Rect();
+ private Paint mPaint = new Paint();
+ private Paint mEventPaint = new Paint();
+ private Paint mSelectionPaint = new Paint();
+ private Path mPath = new Path();
+
+ protected boolean mDrawTextInEventRect;
+ private int mStartDay;
+
+ private PopupWindow mPopup;
+ private View mPopupView;
+ private static final int POPUP_HEIGHT = 62;
+
+ // The number of milliseconds to show the popup window
+ private static final int POPUP_DISMISS_DELAY = 3000;
+ private DismissPopup mDismissPopup = new DismissPopup();
+
+ // For drawing to an off-screen Canvas
+ private Bitmap mBitmap;
+ private Canvas mCanvas;
+ private boolean mRedrawScreen = true;
+ private boolean mRemeasure = true;
+
+ private final EventLoader mEventLoader;
+ protected final EventGeometry mEventGeometry;
+
+ private static final int DAY_GAP = 1;
+ private static final int HOUR_GAP = 1;
+ private static final int SINGLE_ALLDAY_HEIGHT = 20;
+ private static final int MAX_ALLDAY_HEIGHT = 72;
+ private static final int ALLDAY_TOP_MARGIN = 3;
+ private static final int MAX_ALLDAY_EVENT_HEIGHT = 18;
+
+ /* The extra space to leave above the text in all-day events */
+ private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
+
+ /* The extra space to leave above the text in normal events */
+ private static final int NORMAL_TEXT_TOP_MARGIN = 2;
+
+ private static final int HOURS_LEFT_MARGIN = 2;
+ private static final int HOURS_RIGHT_MARGIN = 4;
+ private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
+
+ /* package */ static final int MINUTES_PER_HOUR = 60;
+ /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
+ /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
+ /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
+ /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
+
+ private static final int NORMAL_FONT_SIZE = 12;
+ private static final int EVENT_TEXT_FONT_SIZE = 12;
+ private static final int HOURS_FONT_SIZE = 12;
+ private static final int AMPM_FONT_SIZE = 9;
+ private static final int MIN_CELL_WIDTH_FOR_TEXT = 10;
+ private static final int MAX_EVENT_TEXT_LEN = 500;
+ private static final float MIN_EVENT_HEIGHT = 15.0F; // in pixels
+
+ private static final float CALENDAR_COLOR_WIDTH = 8.0F;
+ private static final float CALENDAR_COLOR_HEIGHT_OFFSET = 6.0F;
+
+ private static int mSelectionColor;
+ private static int mAllDayEventColor;
+
+ private int mViewStartX;
+ private int mViewStartY;
+ private int mMaxViewStartY;
+ private int mBitmapHeight;
+ private int mViewHeight;
+ private int mViewWidth;
+ private int mGridAreaHeight;
+ private int mGridAreaWidth;
+ private int mCellHeight;
+ private int mScrollStartY;
+ private int mPreviousDirection;
+ private int mPreviousDistanceX;
+
+ private int mHoursTextHeight;
+ private int mEventTextAscent;
+ private int mEventTextHeight;
+ private int mAllDayHeight;
+ private int mBannerPlusMargin;
+ private int mMaxAllDayEvents;
+
+ protected int mNumDays = 7;
+ private int mNumHours = 10;
+ private int mHoursWidth;
+ private int mDateStrWidth;
+ private int mFirstCell;
+ private int mFirstHour = -1;
+ private int mFirstHourOffset;
+ private String[] mHourStrs;
+ private String[] mDayStrs;
+ private String[] mDayStrs2Letter;
+ private boolean mIs24HourFormat;
+
+ private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
+ private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
+ private boolean mComputeSelectedEvents;
+ private Event mSelectedEvent;
+ private Event mPrevSelectedEvent;
+ private Rect mPrevBox = new Rect();
+ protected final Resources mResources;
+ private String mAmString;
+ private String mPmString;
+ private DeleteEventHelper mDeleteEventHelper;
+
+ private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
+
+ /**
+ * The initial state of the touch mode when we enter this view.
+ */
+ private static final int TOUCH_MODE_INITIAL_STATE = 0;
+
+ /**
+ * Indicates we just received the touch event and we are waiting to see if
+ * it is a tap or a scroll gesture.
+ */
+ private static final int TOUCH_MODE_DOWN = 1;
+
+ /**
+ * Indicates the touch gesture is a vertical scroll
+ */
+ private static final int TOUCH_MODE_VSCROLL = 0x20;
+
+ /**
+ * Indicates the touch gesture is a horizontal scroll
+ */
+ private static final int TOUCH_MODE_HSCROLL = 0x40;
+
+ private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
+
+ /**
+ * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
+ */
+ private static final int SELECTION_HIDDEN = 0;
+ private static final int SELECTION_PRESSED = 1;
+ private static final int SELECTION_SELECTED = 2;
+ private static final int SELECTION_LONGPRESS = 3;
+
+ private int mSelectionMode = SELECTION_HIDDEN;
+
+ private boolean mScrolling = false;
+
+ private String mDateRange;
+ private TextView mTitleTextView;
+
+ public CalendarView(CalendarActivity activity) {
+ super(activity);
+ mResources = activity.getResources();
+ mEventLoader = activity.mEventLoader;
+ mEventGeometry = new EventGeometry();
+ mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
+ mEventGeometry.setHourGap(HOUR_GAP);
+ mParentActivity = activity;
+ mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
+ mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
+
+ init(activity);
+ }
+
+ private void init(Context context) {
+ setFocusable(true);
+
+ // Allow focus in touch mode so that we can do keyboard shortcuts
+ // even after we've entered touch mode.
+ setFocusableInTouchMode(true);
+ setClickable(true);
+ setOnCreateContextMenuListener(this);
+
+ mStartDay = Calendar.getInstance().getFirstDayOfWeek();
+ if (mStartDay == Calendar.SATURDAY) {
+ mStartDay = Time.SATURDAY;
+ } else if (mStartDay == Calendar.MONDAY) {
+ mStartDay = Time.MONDAY;
+ } else {
+ mStartDay = Time.SUNDAY;
+ }
+
+ mSelectionColor = mResources.getColor(R.color.selection);
+ mAllDayEventColor = mResources.getColor(R.color.calendar_all_day_event_color);
+ int eventTextColor = mResources.getColor(R.color.calendar_event_text_color);
+ mEventPaint.setColor(eventTextColor);
+ mEventPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
+ mEventPaint.setTextAlign(Paint.Align.LEFT);
+ mEventPaint.setAntiAlias(true);
+
+ int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
+ Paint p = mSelectionPaint;
+ p.setColor(gridLineColor);
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(2.0f);
+ p.setAntiAlias(false);
+
+ p = mPaint;
+ p.setAntiAlias(true);
+
+ // Allocate space for 2 weeks worth of weekday names so that we can
+ // easily start the week display at any week day.
+ mDayStrs = new String[14];
+
+ // Also create an array of 2-letter abbreviations.
+ mDayStrs2Letter = new String[14];
+
+ for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
+ int index = i - Calendar.SUNDAY;
+ // e.g. Tue for Tuesday
+ mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
+ mDayStrs[index + 7] = mDayStrs[index];
+ // e.g. Tu for Tuesday
+ mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
+ mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
+ }
+
+ // Figure out how much space we need for the 3-letter abbrev names
+ // in the worst case.
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTypeface(mBold);
+ String[] dateStrs = {" 28", " 30"};
+ mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
+ mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
+
+ p.setTextSize(HOURS_FONT_SIZE);
+ p.setTypeface(null);
+ mIs24HourFormat = DateFormat.is24HourFormat(context);
+ mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
+ mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
+
+ mAmString = DateUtils.getAMPMString(Calendar.AM);
+ mPmString = DateUtils.getAMPMString(Calendar.PM);
+ String[] ampm = {mAmString, mPmString};
+ p.setTextSize(AMPM_FONT_SIZE);
+ mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
+ mHoursWidth += HOURS_MARGIN;
+ mBoxNormal = mResources.getDrawable(R.drawable.box_appointment_normal);
+ mBoxSelected = mResources.getDrawable(R.drawable.box_appointment_selected);
+ mBoxPressed = mResources.getDrawable(R.drawable.box_appointment_pressed);
+ mBoxLongPressed = mResources.getDrawable(R.drawable.box_appointment_longpress);
+
+ LayoutInflater inflater;
+ inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPopupView = inflater.inflate(R.layout.bubble_event, null);
+ mPopup = new PopupWindow(context);
+ mPopup.setContentView(mPopupView);
+ Resources.Theme dialogTheme = getResources().newTheme();
+ dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
+ TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
+ android.R.attr.windowBackground });
+ mPopup.setBackgroundDrawable(ta.getDrawable(0));
+ ta.recycle();
+
+ // Enable touching the popup window
+ mPopupView.setOnClickListener(this);
+
+ mBaseDate = new Time();
+ long millis = System.currentTimeMillis();
+ mBaseDate.set(millis);
+
+ mEarliestStartHour = new int[mNumDays];
+ mHasAllDayEvent = new boolean[mNumDays];
+
+ mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
+ mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
+ }
+
+ /**
+ * This is called when the popup window is pressed.
+ */
+ public void onClick(View v) {
+ if (v == mPopupView) {
+ // Pretend it was a trackball click because that will always
+ // jump to the "View event" screen.
+ switchViews(true /* trackball */);
+ }
+ }
+
+ /**
+ * Returns the start of the selected time in milliseconds since the epoch.
+ *
+ * @return selected time in UTC milliseconds since the epoch.
+ */
+ long getSelectedTimeInMillis() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ return time.normalize(true /* ignore isDst */);
+ }
+
+ Time getSelectedTime() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */);
+ return time;
+ }
+
+ /**
+ * Returns the start of the selected time in minutes since midnight,
+ * local time. The derived class must ensure that this is consistent
+ * with the return value from getSelectedTimeInMillis().
+ */
+ int getSelectedMinutesSinceMidnight() {
+ return mSelectionHour * MINUTES_PER_HOUR;
+ }
+
+ public void setSelectedDay(Time time) {
+ mBaseDate.set(time);
+ mSelectionHour = mBaseDate.hour;
+ mSelectedEvent = null;
+ mPrevSelectedEvent = null;
+ long millis = mBaseDate.toMillis(false /* use isDst */);
+ mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+
+ // Force a recalculation of the first visible hour
+ mFirstHour = -1;
+ recalc();
+ mTitleTextView.setText(mDateRange);
+
+ // Force a redraw of the selection box.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ mRemeasure = true;
+ invalidate();
+ }
+
+ public Time getSelectedDay() {
+ Time time = new Time(mBaseDate);
+ time.setJulianDay(mSelectionDay);
+ time.hour = mSelectionHour;
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */);
+ return time;
+ }
+
+ private void recalc() {
+ // Set the base date to the beginning of the week if we are displaying
+ // 7 days at a time.
+ if (mNumDays == 7) {
+ int dayOfWeek = mBaseDate.weekDay;
+ int diff = dayOfWeek - mStartDay;
+ if (diff != 0) {
+ if (diff < 0) {
+ diff += 7;
+ }
+ mBaseDate.monthDay -= diff;
+ mBaseDate.normalize(true /* ignore isDst */);
+ }
+ }
+
+ final long start = mBaseDate.toMillis(false /* use isDst */);
+ long end = start;
+ mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
+ mLastJulianDay = mFirstJulianDay + mNumDays - 1;
+
+ mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
+ mFirstDate = mBaseDate.monthDay;
+
+ int flags = DateUtils.FORMAT_SHOW_YEAR;
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ if (mNumDays > 1) {
+ mBaseDate.monthDay += mNumDays - 1;
+ end = mBaseDate.toMillis(true /* ignore isDst */);
+ mBaseDate.monthDay -= mNumDays - 1;
+ flags |= DateUtils.FORMAT_NO_MONTH_DAY;
+ } else {
+ flags |= DateUtils.FORMAT_SHOW_WEEKDAY
+ | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
+ }
+
+ mDateRange = DateUtils.formatDateRange(start, end, flags);
+ // Do not set the title here because this is called when executing
+ // initNextView() to prepare the Day view when sliding the finger
+ // horizontally but we don't always want to change the title. And
+ // if we change the title here and then change it back in the caller
+ // then we get an annoying flicker.
+ }
+
+ void setDetailedView(String detailedView) {
+ mDetailedView = detailedView;
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldw, int oldh) {
+ mViewWidth = width;
+ mViewHeight = height;
+ mGridAreaWidth = width - mHoursWidth;
+ mCellWidth = (mGridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
+
+ Paint p = new Paint();
+ p.setTextSize(NORMAL_FONT_SIZE);
+ int bannerTextHeight = (int) Math.abs(p.ascent());
+
+ p.setTextSize(HOURS_FONT_SIZE);
+ mHoursTextHeight = (int) Math.abs(p.ascent());
+
+ p.setTextSize(EVENT_TEXT_FONT_SIZE);
+ float ascent = -p.ascent();
+ mEventTextAscent = (int) Math.ceil(ascent);
+ float totalHeight = ascent + p.descent();
+ mEventTextHeight = (int) Math.ceil(totalHeight);
+
+ if (mNumDays > 1) {
+ mBannerPlusMargin = bannerTextHeight + 14;
+ } else {
+ mBannerPlusMargin = 0;
+ }
+
+ remeasure(width, height);
+ }
+
+ // Measures the space needed for various parts of the view after
+ // loading new events. This can change if there are all-day events.
+ private void remeasure(int width, int height) {
+
+ // First, clear the array of earliest start times, and the array
+ // indicating presence of an all-day event.
+ for (int day = 0; day < mNumDays; day++) {
+ mEarliestStartHour[day] = 25; // some big number
+ mHasAllDayEvent[day] = false;
+ }
+
+ // Compute the space needed for the all-day events, if any.
+ // Make a pass over all the events, and keep track of the maximum
+ // number of all-day events in any one day. Also, keep track of
+ // the earliest event in each day.
+ int maxAllDayEvents = 0;
+ ArrayList<Event> events = mEvents;
+ int len = events.size();
+ for (int ii = 0; ii < len; ii++) {
+ Event event = events.get(ii);
+ if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
+ continue;
+ if (event.allDay) {
+ int max = event.getColumn() + 1;
+ if (maxAllDayEvents < max) {
+ maxAllDayEvents = max;
+ }
+ int daynum = event.startDay - mFirstJulianDay;
+ int durationDays = event.endDay - event.startDay + 1;
+ if (daynum < 0) {
+ durationDays += daynum;
+ daynum = 0;
+ }
+ if (daynum + durationDays > mNumDays) {
+ durationDays = mNumDays - daynum;
+ }
+ for (int day = daynum; durationDays > 0; day++, durationDays--) {
+ mHasAllDayEvent[day] = true;
+ }
+ } else {
+ int daynum = event.startDay - mFirstJulianDay;
+ int hour = event.startTime / 60;
+ if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
+ mEarliestStartHour[daynum] = hour;
+ }
+
+ // Also check the end hour in case the event spans more than
+ // one day.
+ daynum = event.endDay - mFirstJulianDay;
+ hour = event.endTime / 60;
+ if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
+ mEarliestStartHour[daynum] = hour;
+ }
+ }
+ }
+ mMaxAllDayEvents = maxAllDayEvents;
+
+ mFirstCell = mBannerPlusMargin;
+ int allDayHeight = 0;
+ if (maxAllDayEvents > 0) {
+ // If there is at most one all-day event per day, then use less
+ // space (but more than the space for a single event).
+ if (maxAllDayEvents == 1) {
+ allDayHeight = SINGLE_ALLDAY_HEIGHT;
+ } else {
+ // Allow the all-day area to grow in height depending on the
+ // number of all-day events we need to show, up to a limit.
+ allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
+ if (allDayHeight > MAX_ALLDAY_HEIGHT) {
+ allDayHeight = MAX_ALLDAY_HEIGHT;
+ }
+ }
+ mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
+ } else {
+ mSelectionAllDay = false;
+ }
+ mAllDayHeight = allDayHeight;
+
+ mGridAreaHeight = height - mFirstCell;
+ mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
+ int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
+ int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
+ mEventGeometry.setHourHeight(mCellHeight);
+
+ // Create an off-screen bitmap that we can draw into.
+ mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
+ if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
+ mBitmapHeight > 0) {
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
+ mCanvas = new Canvas(mBitmap);
+ }
+ mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
+
+ if (mFirstHour == -1) {
+ initFirstHour();
+ mFirstHourOffset = 0;
+ }
+
+ // When we change the base date, the number of all-day events may
+ // change and that changes the cell height. When we switch dates,
+ // we use the mFirstHourOffset from the previous view, but that may
+ // be too large for the new view if the cell height is smaller.
+ if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
+ mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
+ }
+ mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
+
+ int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
+ mPopup.dismiss();
+ mPopup.setWidth(eventAreaWidth - 20);
+ mPopup.setHeight(POPUP_HEIGHT);
+ }
+
+ /**
+ * Initialize the state for another view. The given view is one that has
+ * its own bitmap and will use an animation to replace the current view.
+ * The current view and new view are either both Week views or both Day
+ * views. They differ in their base date.
+ *
+ * @param view the view to initialize.
+ */
+ private void initView(CalendarView view) {
+ view.mSelectionHour = mSelectionHour;
+ view.mSelectedEvents.clear();
+ view.mComputeSelectedEvents = true;
+ view.mFirstHour = mFirstHour;
+ view.mFirstHourOffset = mFirstHourOffset;
+ view.remeasure(getWidth(), getHeight());
+
+ view.mSelectedEvent = null;
+ view.mPrevSelectedEvent = null;
+ view.mStartDay = mStartDay;
+ if (view.mEvents.size() > 0) {
+ view.mSelectionAllDay = mSelectionAllDay;
+ } else {
+ view.mSelectionAllDay = false;
+ }
+
+ // Redraw the screen so that the selection box will be redrawn. We may
+ // have scrolled to a different part of the day in some other view
+ // so the selection box in this view may no longer be visible.
+ view.mRedrawScreen = true;
+ view.recalc();
+ }
+
+ /**
+ * Switch to another view based on what was selected (an event or a free
+ * slot) and how it was selected (by touch or by trackball).
+ *
+ * @param trackBallSelection true if the selection was made using the
+ * trackball.
+ */
+ private void switchViews(boolean trackBallSelection) {
+ Event selectedEvent = mSelectedEvent;
+
+ mPopup.dismiss();
+ if (mNumDays > 1) {
+ // This is the Week view.
+ // With touch, we always switch to Day/Agenda View
+ // With track ball, if we selected a free slot, then create an event.
+ // If we selected a specific event, switch to EventInfo view.
+ if (trackBallSelection) {
+ if (selectedEvent == null) {
+ // Switch to the EditEvent view
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
+ selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ } else {
+ // This was a touch selection. If the touch selected a single
+ // unambiguous event, then view that event. Otherwise go to
+ // Day/Agenda view.
+ if (mSelectedEvents.size() == 1) {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
+ selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the Day/Agenda view.
+ long millis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, mDetailedView, millis);
+ mParentActivity.finish();
+ }
+ }
+ } else {
+ // This is the Day view.
+ // If we selected a free slot, then create an event.
+ // If we selected an event, then go to the EventInfo view.
+ if (selectedEvent == null) {
+ // Switch to the EditEvent view
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ mParentActivity.startActivity(intent);
+ } else {
+ // Switch to the EventInfo view
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ mScrolling = false;
+ long duration = event.getEventTime() - event.getDownTime();
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ // Don't do anything unless the selection is visible.
+ break;
+ }
+
+ if (mSelectionMode == SELECTION_PRESSED) {
+ // This was the first press when there was nothing selected.
+ // Change the selection from the "pressed" state to the
+ // the "selected" state. We treat short-press and
+ // long-press the same here because nothing was selected.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ break;
+ }
+
+ // Check the duration to determine if this was a short press
+ if (duration < ViewConfiguration.getLongPressTimeout()) {
+ switchViews(true /* trackball */);
+ } else {
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+ break;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mSelectionMode == SELECTION_HIDDEN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+ || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
+ || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // Display the selection box but don't move or select it
+ // on this key press.
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ // Display the selection box but don't select it
+ // on this key press.
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ }
+ }
+
+ mSelectionMode = SELECTION_SELECTED;
+ mScrolling = false;
+ boolean redraw = false;
+ int selectionDay = mSelectionDay;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DEL:
+ // Delete the selected event, if any
+ Event selectedEvent = mSelectedEvent;
+ if (selectedEvent == null) {
+ return false;
+ }
+ mPopup.dismiss();
+
+ long begin = selectedEvent.startMillis;
+ long end = selectedEvent.endMillis;
+ long id = selectedEvent.id;
+ mDeleteEventHelper.delete(begin, end, id, -1);
+ return true;
+ case KeyEvent.KEYCODE_ENTER:
+ switchViews(true /* trackball or keyboard */);
+ return true;
+ case KeyEvent.KEYCODE_BACK:
+ mPopup.dismiss();
+ mParentActivity.finish();
+ return true;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextLeft;
+ }
+ if (mSelectedEvent == null) {
+ selectionDay -= 1;
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextRight;
+ }
+ if (mSelectedEvent == null) {
+ selectionDay += 1;
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextUp;
+ }
+ if (mSelectedEvent == null) {
+ if (!mSelectionAllDay) {
+ mSelectionHour -= 1;
+ adjustHourSelection();
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+ redraw = true;
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (mSelectedEvent != null) {
+ mSelectedEvent = mSelectedEvent.nextDown;
+ }
+ if (mSelectedEvent == null) {
+ if (mSelectionAllDay) {
+ mSelectionAllDay = false;
+ } else {
+ mSelectionHour++;
+ adjustHourSelection();
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+ redraw = true;
+ break;
+
+ default:
+ return super.onKeyDown(keyCode, event);
+ }
+
+ if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
+ boolean forward;
+ CalendarView view = mParentActivity.getNextView();
+ Time date = view.mBaseDate;
+ date.set(mBaseDate);
+ if (selectionDay < mFirstJulianDay) {
+ date.monthDay -= mNumDays;
+ forward = false;
+ } else {
+ date.monthDay += mNumDays;
+ forward = true;
+ }
+ date.normalize(true /* ignore isDst */);
+ view.mSelectionDay = selectionDay;
+
+ initView(view);
+ mTitleTextView.setText(view.mDateRange);
+ mParentActivity.switchViews(forward, 0, 0);
+ return true;
+ }
+ mSelectionDay = selectionDay;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+
+ if (redraw) {
+ mRedrawScreen = true;
+ invalidate();
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ // This is called after scrolling stops to move the selected hour
+ // to the visible part of the screen.
+ private void resetSelectedHour() {
+ if (mSelectionHour < mFirstHour + 1) {
+ mSelectionHour = mFirstHour + 1;
+ mSelectedEvent = null;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ mSelectionHour = mFirstHour + mNumHours - 3;
+ mSelectedEvent = null;
+ mSelectedEvents.clear();
+ mComputeSelectedEvents = true;
+ }
+ }
+
+ private void initFirstHour() {
+ mFirstHour = mSelectionHour - mNumHours / 2;
+ if (mFirstHour < 0) {
+ mFirstHour = 0;
+ } else if (mFirstHour + mNumHours > 24) {
+ mFirstHour = 24 - mNumHours;
+ }
+ }
+
+ /**
+ * Recomputes the first full hour that is visible on screen after the
+ * screen is scrolled.
+ */
+ private void computeFirstHour() {
+ // Compute the first full hour that is visible on screen
+ mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
+ mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
+ }
+
+ private void adjustHourSelection() {
+ if (mSelectionHour < 0) {
+ mSelectionHour = 0;
+ if (mMaxAllDayEvents > 0) {
+ mPrevSelectedEvent = null;
+ mSelectionAllDay = true;
+ }
+ }
+
+ if (mSelectionHour > 23) {
+ mSelectionHour = 23;
+ }
+
+ // If the selected hour is at least 2 time slots from the top and
+ // bottom of the screen, then don't scroll the view.
+ if (mSelectionHour < mFirstHour + 1) {
+ // If there are all-days events for the selected day but there
+ // are no more normal events earlier in the day, then jump to
+ // the all-day event area.
+ // Exception 1: allow the user to scroll to 8am with the trackball
+ // before jumping to the all-day event area.
+ // Exception 2: if 12am is on screen, then allow the user to select
+ // 12am before going up to the all-day event area.
+ int daynum = mSelectionDay - mFirstJulianDay;
+ if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
+ && mFirstHour > 0 && mFirstHour < 8) {
+ mPrevSelectedEvent = null;
+ mSelectionAllDay = true;
+ mSelectionHour = mFirstHour + 1;
+ return;
+ }
+
+ if (mFirstHour > 0) {
+ mFirstHour -= 1;
+ mViewStartY -= (mCellHeight + HOUR_GAP);
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ }
+ return;
+ }
+ }
+
+ if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ if (mFirstHour < 24 - mNumHours) {
+ mFirstHour += 1;
+ mViewStartY += (mCellHeight + HOUR_GAP);
+ if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
+ mViewStartY = mBitmapHeight - mGridAreaHeight;
+ }
+ return;
+ } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
+ mViewStartY = mBitmapHeight - mGridAreaHeight;
+ }
+ }
+ }
+
+ void clearCachedEvents() {
+ mLastReloadMillis = 0;
+ }
+
+ private Runnable mCancelCallback = new Runnable() {
+ public void run() {
+ clearCachedEvents();
+ }
+ };
+
+ void reloadEvents() {
+ // Protect against this being called before this view has been
+ // initialized.
+ if (mParentActivity == null) {
+ return;
+ }
+
+ mSelectedEvent = null;
+ mPrevSelectedEvent = null;
+ mSelectedEvents.clear();
+
+ // The start date is the beginning of the week at 12am
+ Time weekStart = new Time();
+ weekStart.set(mBaseDate);
+ weekStart.hour = 0;
+ weekStart.minute = 0;
+ weekStart.second = 0;
+ long millis = weekStart.normalize(true /* ignore isDst */);
+
+ // Avoid reloading events unnecessarily.
+ if (millis == mLastReloadMillis) {
+ return;
+ }
+ mLastReloadMillis = millis;
+
+ // load events in the background
+ mParentActivity.startProgressSpinner();
+ final ArrayList<Event> events = new ArrayList<Event>();
+ mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
+ public void run() {
+ mEvents = events;
+ mRemeasure = true;
+ mRedrawScreen = true;
+ mComputeSelectedEvents = true;
+ recalc();
+ mParentActivity.stopProgressSpinner();
+ invalidate();
+ }
+ }, mCancelCallback);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mRemeasure) {
+ remeasure(getWidth(), getHeight());
+ mRemeasure = false;
+ }
+
+ if (mRedrawScreen && mCanvas != null) {
+ doDraw(mCanvas);
+ mRedrawScreen = false;
+ }
+
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ canvas.save();
+ if (mViewStartX > 0) {
+ canvas.translate(mViewWidth - mViewStartX, 0);
+ } else {
+ canvas.translate(-(mViewWidth + mViewStartX), 0);
+ }
+ CalendarView nextView = mParentActivity.getNextView();
+
+ // Prevent infinite recursive calls to onDraw().
+ nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
+
+ nextView.onDraw(canvas);
+ canvas.restore();
+ canvas.save();
+ canvas.translate(-mViewStartX, 0);
+ }
+
+ if (mBitmap != null) {
+ drawCalendarView(canvas);
+ }
+
+ // Draw the fixed areas (that don't scroll) directly to the canvas.
+ drawAfterScroll(canvas);
+ mComputeSelectedEvents = false;
+
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ canvas.restore();
+ }
+ }
+
+ private void drawCalendarView(Canvas canvas) {
+
+ // Copy the scrollable region from the big bitmap to the canvas.
+ Rect src = mSrcRect;
+ Rect dest = mDestRect;
+
+ src.top = mViewStartY;
+ src.bottom = mViewStartY + mGridAreaHeight;
+ src.left = 0;
+ src.right = mViewWidth;
+
+ dest.top = mFirstCell;
+ dest.bottom = mViewHeight;
+ dest.left = 0;
+ dest.right = mViewWidth;
+ canvas.save();
+ canvas.clipRect(dest);
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ canvas.drawBitmap(mBitmap, src, dest, null);
+ canvas.restore();
+ }
+
+ private void drawAfterScroll(Canvas canvas) {
+ Paint p = mPaint;
+ Rect r = mRect;
+
+ if (mMaxAllDayEvents != 0) {
+ drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
+ drawUpperLeftCorner(r, canvas, p);
+ }
+
+ if (mNumDays > 1) {
+ drawDayHeaderLoop(r, canvas, p);
+ }
+
+ // Draw the AM and PM indicators if we're in 12 hour mode
+ if (!mIs24HourFormat) {
+ drawAmPm(canvas, p);
+ }
+
+ // Update the popup window showing the event details, but only if
+ // we are not scrolling and we have focus.
+ if (!mScrolling && isFocused()) {
+ updateEventDetails();
+ }
+ }
+
+ // This isn't really the upper-left corner. It's the square area just
+ // below the upper-left corner, above the hours and to the left of the
+ // all-day area.
+ private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
+ p.setColor(mResources.getColor(R.color.calendar_hour_background));
+ r.top = mBannerPlusMargin;
+ r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+ }
+
+ private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
+ // Draw the horizontal day background banner
+ p.setColor(mResources.getColor(R.color.calendar_date_banner_background));
+ r.top = 0;
+ r.bottom = mBannerPlusMargin;
+ r.left = 0;
+ r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
+ canvas.drawRect(r, p);
+
+ // Fill the extra space on the right side with the default background
+ r.left = r.right;
+ r.right = mViewWidth;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw a highlight on the selected day (if any), but only if we are
+ // displaying more than one day.
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ if (mNumDays > 1) {
+ p.setColor(mResources.getColor(R.color.calendar_date_selected));
+ r.top = 0;
+ r.bottom = mBannerPlusMargin;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ r.right = r.left + mCellWidth;
+ canvas.drawRect(r, p);
+ }
+ }
+
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTextAlign(Paint.Align.CENTER);
+ int x = mHoursWidth;
+ int deltaX = mCellWidth + DAY_GAP;
+ int cell = mFirstJulianDay;
+
+ String[] dayNames;
+ if (mDateStrWidth < mCellWidth) {
+ dayNames = mDayStrs;
+ } else {
+ dayNames = mDayStrs2Letter;
+ }
+
+ for (int day = 0; day < mNumDays; day++, cell++) {
+ drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
+ x += deltaX;
+ }
+ }
+
+ private void drawAmPm(Canvas canvas, Paint p) {
+ p.setColor(mResources.getColor(R.color.calendar_ampm_label));
+ p.setTextSize(AMPM_FONT_SIZE);
+ p.setTypeface(mBold);
+ p.setAntiAlias(true);
+ mPaint.setTextAlign(Paint.Align.RIGHT);
+ String text = mAmString;
+ if (mFirstHour >= 12) {
+ text = mPmString;
+ }
+ int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
+ int right = mHoursWidth - HOURS_RIGHT_MARGIN;
+ canvas.drawText(text, right, y, p);
+
+ if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
+ // Also draw the "PM"
+ text = mPmString;
+ y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
+ + 2 * mHoursTextHeight + HOUR_GAP;
+ canvas.drawText(text, right, y, p);
+ }
+ }
+
+ private void doDraw(Canvas canvas) {
+ Paint p = mPaint;
+ Rect r = mRect;
+
+ drawGridBackground(r, canvas, p);
+ drawHours(r, canvas, p);
+
+ // Draw each day
+ int x = mHoursWidth;
+ int deltaX = mCellWidth + DAY_GAP;
+ int cell = mFirstJulianDay;
+ for (int day = 0; day < mNumDays; day++, cell++) {
+ drawEvents(cell, x, HOUR_GAP, canvas, p);
+ x += deltaX;
+ }
+ }
+
+ private void drawHours(Rect r, Canvas canvas, Paint p) {
+ // Draw the background for the hour labels
+ p.setColor(mResources.getColor(R.color.calendar_hour_background));
+ r.top = 0;
+ r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+
+ // Fill the bottom left corner with the default grid background
+ r.top = r.bottom;
+ r.bottom = mBitmapHeight;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw a highlight on the selected hour (if needed)
+ if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
+ p.setColor(mResources.getColor(R.color.calendar_hour_selected));
+ r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
+ r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
+ r.left = 0;
+ r.right = mHoursWidth;
+ canvas.drawRect(r, p);
+
+ // Also draw the highlight on the grid
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_selected));
+ int daynum = mSelectionDay - mFirstJulianDay;
+ r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ r.right = r.left + mCellWidth;
+ canvas.drawRect(r, p);
+
+ // Draw a border around the highlighted grid hour.
+ Path path = mPath;
+ r.top += HOUR_GAP;
+ r.bottom -= HOUR_GAP;
+ path.reset();
+ path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
+ canvas.drawPath(path, mSelectionPaint);
+ saveSelectionPosition(r.left, r.top, r.right, r.bottom);
+ }
+
+ p.setColor(mResources.getColor(R.color.calendar_hour_label));
+ p.setTextSize(HOURS_FONT_SIZE);
+ p.setTypeface(mBold);
+ p.setTextAlign(Paint.Align.RIGHT);
+ p.setAntiAlias(true);
+
+ int right = mHoursWidth - HOURS_RIGHT_MARGIN;
+ int y = HOUR_GAP + mHoursTextHeight;
+
+ for (int i = 0; i < 24; i++) {
+ String time = mHourStrs[i];
+ canvas.drawText(time, right, y, p);
+ y += mCellHeight + HOUR_GAP;
+ }
+ }
+
+ private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
+ float xCenter = x + mCellWidth / 2.0f;
+
+ p.setTypeface(mBold);
+ p.setAntiAlias(true);
+
+ boolean isWeekend = false;
+ if ((mStartDay == Time.SUNDAY && (day == 0 || day == 6))
+ || (mStartDay == Time.MONDAY && (day == 5 || day == 6))
+ || (mStartDay == Time.SATURDAY && (day == 0 || day == 1))) {
+ isWeekend = true;
+ }
+
+ if (isWeekend) {
+ p.setColor(mResources.getColor(R.color.week_weekend));
+ } else {
+ p.setColor(mResources.getColor(R.color.calendar_date_banner_text_color));
+ }
+
+ int dateNum = mFirstDate + day;
+ if (dateNum > mMonthLength) {
+ dateNum -= mMonthLength;
+ }
+
+ // Add a leading zero if the date is a single digit
+ if (dateNum < 10) {
+ dateStr += " 0" + dateNum;
+ } else {
+ dateStr += " " + dateNum;
+ }
+
+ float y = mBannerPlusMargin - 7;
+ canvas.drawText(dateStr, xCenter, y, p);
+ }
+
+ private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
+ Paint.Style savedStyle = p.getStyle();
+
+ // Clear the background
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ r.top = 0;
+ r.bottom = mBitmapHeight;
+ r.left = 0;
+ r.right = mViewWidth;
+ canvas.drawRect(r, p);
+
+ // Draw the horizontal grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_horizontal_color));
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(0);
+ p.setAntiAlias(false);
+ float startX = mHoursWidth;
+ float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
+ float y = 0;
+ float deltaY = mCellHeight + HOUR_GAP;
+ for (int hour = 0; hour <= 24; hour++) {
+ canvas.drawLine(startX, y, stopX, y, p);
+ y += deltaY;
+ }
+
+ // Draw the vertical grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_vertical_color));
+ float startY = 0;
+ float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
+ float deltaX = mCellWidth + DAY_GAP;
+ float x = mHoursWidth + mCellWidth;
+ for (int day = 0; day < mNumDays; day++) {
+ canvas.drawLine(x, startY, x, stopY, p);
+ x += deltaX;
+ }
+
+ // Restore the saved style.
+ p.setStyle(savedStyle);
+ p.setAntiAlias(true);
+ }
+
+ Event getSelectedEvent() {
+ if (mSelectedEvent == null) {
+ // There is no event at the selected hour, so create a new event.
+ return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
+ getSelectedMinutesSinceMidnight());
+ }
+ return mSelectedEvent;
+ }
+
+ boolean isEventSelected() {
+ return (mSelectedEvent != null);
+ }
+
+ Event getNewEvent() {
+ return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
+ getSelectedMinutesSinceMidnight());
+ }
+
+ static Event getNewEvent(int julianDay, long utcMillis,
+ int minutesSinceMidnight) {
+ Event event = Event.newInstance();
+ event.startDay = julianDay;
+ event.endDay = julianDay;
+ event.startMillis = utcMillis;
+ event.endMillis = event.startMillis + MILLIS_PER_HOUR;
+ event.startTime = minutesSinceMidnight;
+ event.endTime = event.startTime + MINUTES_PER_HOUR;
+ return event;
+ }
+
+ private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
+ float maxWidthF = 0.0f;
+
+ int len = strings.length;
+ for (int i = 0; i < len; i++) {
+ float width = p.measureText(strings[i]);
+ maxWidthF = Math.max(width, maxWidthF);
+ }
+ int maxWidth = (int) (maxWidthF + 0.5);
+ if (maxWidth < currentMax) {
+ maxWidth = currentMax;
+ }
+ return maxWidth;
+ }
+
+ private void saveSelectionPosition(float left, float top, float right, float bottom) {
+ mPrevBox.left = (int) left;
+ mPrevBox.right = (int) right;
+ mPrevBox.top = (int) top;
+ mPrevBox.bottom = (int) bottom;
+ }
+
+ private Rect getCurrentSelectionPosition() {
+ Rect box = new Rect();
+ box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
+ box.bottom = box.top + mCellHeight + HOUR_GAP;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
+ box.right = box.left + mCellWidth + DAY_GAP;
+ return box;
+ }
+
+ private void drawAllDayEvents(int firstDay, int numDays,
+ Rect r, Canvas canvas, Paint p) {
+ p.setTextSize(NORMAL_FONT_SIZE);
+ p.setTextAlign(Paint.Align.LEFT);
+ Paint eventPaint = mEventPaint;
+
+ // Draw the background for the all-day events area
+ r.top = mBannerPlusMargin;
+ r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
+ r.left = mHoursWidth;
+ r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
+ p.setColor(mResources.getColor(R.color.calendar_all_day_background));
+ canvas.drawRect(r, p);
+
+ // Fill the extra space on the right side with the default background
+ r.left = r.right;
+ r.right = mViewWidth;
+ p.setColor(mResources.getColor(R.color.calendar_grid_area_background));
+ canvas.drawRect(r, p);
+
+ // Draw the vertical grid lines
+ p.setColor(mResources.getColor(R.color.calendar_grid_line_vertical_color));
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(0);
+ p.setAntiAlias(false);
+ float startY = r.top;
+ float stopY = r.bottom;
+ float deltaX = mCellWidth + DAY_GAP;
+ float x = mHoursWidth + mCellWidth;
+ for (int day = 0; day <= mNumDays; day++) {
+ canvas.drawLine(x, startY, x, stopY, p);
+ x += deltaX;
+ }
+ p.setAntiAlias(true);
+ p.setStyle(Style.FILL);
+
+ int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
+ float left = mHoursWidth;
+ int lastDay = firstDay + numDays - 1;
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ float drawHeight = mAllDayHeight;
+ float numRectangles = mMaxAllDayEvents;
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!event.allDay)
+ continue;
+ int startDay = event.startDay;
+ int endDay = event.endDay;
+ if (startDay > lastDay || endDay < firstDay)
+ continue;
+ if (startDay < firstDay)
+ startDay = firstDay;
+ if (endDay > lastDay)
+ endDay = lastDay;
+ int startIndex = startDay - firstDay;
+ int endIndex = endDay - firstDay;
+ float height = drawHeight / numRectangles;
+
+ // Prevent a single event from getting too big
+ if (height > MAX_ALLDAY_EVENT_HEIGHT) {
+ height = MAX_ALLDAY_EVENT_HEIGHT;
+ }
+
+ // Leave a one-pixel space between the vertical day lines and the
+ // event rectangle.
+ event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
+ event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
+ event.top = y + height * event.getColumn();
+
+ // Multiply the height by 0.9 to leave a little gap between events
+ event.bottom = event.top + height * 0.9f;
+
+ RectF rf = drawAllDayEventRect(event, canvas, p);
+ drawEventText(event, rf, canvas, eventPaint, ALL_DAY_TEXT_TOP_MARGIN);
+
+ // Check if this all-day event intersects the selected day
+ if (mSelectionAllDay && mComputeSelectedEvents) {
+ if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
+ mSelectedEvents.add(event);
+ }
+ }
+ }
+
+ if (mSelectionAllDay) {
+ // Compute the neighbors for the list of all-day events that
+ // intersect the selected day.
+ computeAllDayNeighbors();
+ if (mSelectedEvent != null) {
+ Event event = mSelectedEvent;
+ RectF rf = drawAllDayEventRect(event, canvas, p);
+ drawEventText(event, rf, canvas, eventPaint, ALL_DAY_TEXT_TOP_MARGIN);
+ }
+
+ // Draw the highlight on the selected all-day area
+ float top = mBannerPlusMargin + 1;
+ float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
+ int daynum = mSelectionDay - mFirstJulianDay;
+ left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
+ float right = left + mCellWidth + DAY_GAP - 1;
+ if (mNumDays == 1) {
+ // The Day view doesn't have a vertical line on the right.
+ right -= 1;
+ }
+ Path path = mPath;
+ path.reset();
+ path.addRect(left, top, right, bottom, Direction.CW);
+ canvas.drawPath(path, mSelectionPaint);
+
+ // Set the selection position to zero so that when we move down
+ // to the normal event area, we will highlight the topmost event.
+ saveSelectionPosition(0f, 0f, 0f, 0f);
+ }
+ }
+
+ private void computeAllDayNeighbors() {
+ int len = mSelectedEvents.size();
+ if (len == 0 || mSelectedEvent != null) {
+ return;
+ }
+
+ // First, clear all the links
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ ev.nextUp = null;
+ ev.nextDown = null;
+ ev.nextLeft = null;
+ ev.nextRight = null;
+ }
+
+ // For each event in the selected event list "mSelectedEvents", find
+ // its neighbors in the up and down directions. This could be done
+ // more efficiently by sorting on the Event.getColumn() field, but
+ // the list is expected to be very small.
+
+ // Find the event in the same row as the previously selected all-day
+ // event, if any.
+ int startPosition = -1;
+ if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
+ startPosition = mPrevSelectedEvent.getColumn();
+ }
+ int maxPosition = -1;
+ Event startEvent = null;
+ Event maxPositionEvent = null;
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ int position = ev.getColumn();
+ if (position == startPosition) {
+ startEvent = ev;
+ } else if (position > maxPosition) {
+ maxPositionEvent = ev;
+ maxPosition = position;
+ }
+ for (int jj = 0; jj < len; jj++) {
+ if (jj == ii) {
+ continue;
+ }
+ Event neighbor = mSelectedEvents.get(jj);
+ int neighborPosition = neighbor.getColumn();
+ if (neighborPosition == position - 1) {
+ ev.nextUp = neighbor;
+ } else if (neighborPosition == position + 1) {
+ ev.nextDown = neighbor;
+ }
+ }
+ }
+ if (startEvent != null) {
+ mSelectedEvent = startEvent;
+ } else {
+ mSelectedEvent = maxPositionEvent;
+ }
+ }
+
+ RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p) {
+ // If this event is selected, then use the selection color
+ if (mSelectedEvent == event) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ p.setColor(mSelectionColor);
+ } else {
+ // Use the normal color for all-day events
+ p.setColor(mAllDayEventColor);
+ }
+
+ RectF rf = mRectF;
+ rf.top = event.top;
+ rf.bottom = event.bottom;
+ rf.left = event.left;
+ rf.right = event.right;
+ canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
+
+ // Draw the calendar color inset rectangle
+ p.setColor(event.color);
+
+ // Save the outer rectangle coordinates so that we can restore them
+ float right = rf.right;
+ float top = rf.top;
+ float bottom = rf.bottom;
+
+ rf.right = rf.left + CALENDAR_COLOR_WIDTH;
+ float eventHeight = rf.bottom - rf.top;
+ rf.top += 0.05f * eventHeight;
+ rf.bottom -= 0.05f * eventHeight;
+ canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
+
+ // Change the rf coordinates to be the area suitable for text.
+ rf.left = rf.right;
+ rf.right = right;
+ rf.top = top;
+ rf.bottom = bottom;
+ return rf;
+ }
+
+ private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
+ Paint eventPaint = mEventPaint;
+ int cellWidth = mCellWidth;
+ int cellHeight = mCellHeight;
+
+ // Use the selected hour as the selection region
+ Rect selectionArea = mRect;
+ selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
+ selectionArea.bottom = selectionArea.top + cellHeight;
+ selectionArea.left = left;
+ selectionArea.right = selectionArea.left + cellWidth;
+
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ EventGeometry geometry = mEventGeometry;
+
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
+ continue;
+ }
+
+ if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
+ && geometry.eventIntersectsSelection(event, selectionArea)) {
+ mSelectedEvents.add(event);
+ }
+
+ RectF rf = drawEventRect(event, canvas, p);
+ drawEventText(event, rf, canvas, eventPaint, NORMAL_TEXT_TOP_MARGIN);
+ }
+
+ if (date == mSelectionDay && !mSelectionAllDay && isFocused()
+ && mSelectionMode != SELECTION_HIDDEN) {
+ computeNeighbors();
+ if (mSelectedEvent != null) {
+ RectF rf = drawEventRect(mSelectedEvent, canvas, p);
+ drawEventText(mSelectedEvent, rf, canvas, eventPaint, NORMAL_TEXT_TOP_MARGIN);
+ }
+ }
+ }
+
+ // Computes the "nearest" neighbor event in four directions (left, right,
+ // up, down) for each of the events in the mSelectedEvents array.
+ private void computeNeighbors() {
+ int len = mSelectedEvents.size();
+ if (len == 0 || mSelectedEvent != null) {
+ return;
+ }
+
+ // First, clear all the links
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+ ev.nextUp = null;
+ ev.nextDown = null;
+ ev.nextLeft = null;
+ ev.nextRight = null;
+ }
+
+ Event startEvent = mSelectedEvents.get(0);
+ int startEventDistance1 = 100000; // any large number
+ int startEventDistance2 = 100000; // any large number
+ int prevLocation = FROM_NONE;
+ int prevTop = 0;
+ int prevBottom = 0;
+ int prevLeft = 0;
+ int prevRight = 0;
+ int prevCenter = 0;
+ Rect box = getCurrentSelectionPosition();
+ if (mPrevSelectedEvent != null) {
+ prevTop = (int) mPrevSelectedEvent.top;
+ prevBottom = (int) mPrevSelectedEvent.bottom;
+ prevLeft = (int) mPrevSelectedEvent.left;
+ prevRight = (int) mPrevSelectedEvent.right;
+ // Check if the previously selected event intersects the previous
+ // selection box. (The previously selected event may be from a
+ // much older selection box.)
+ if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
+ || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
+ mPrevSelectedEvent = null;
+ prevTop = mPrevBox.top;
+ prevBottom = mPrevBox.bottom;
+ prevLeft = mPrevBox.left;
+ prevRight = mPrevBox.right;
+ } else {
+ // Clip the top and bottom to the previous selection box.
+ if (prevTop < mPrevBox.top) {
+ prevTop = mPrevBox.top;
+ }
+ if (prevBottom > mPrevBox.bottom) {
+ prevBottom = mPrevBox.bottom;
+ }
+ }
+ } else {
+ // Just use the previously drawn selection box
+ prevTop = mPrevBox.top;
+ prevBottom = mPrevBox.bottom;
+ prevLeft = mPrevBox.left;
+ prevRight = mPrevBox.right;
+ }
+
+ // Figure out where we came from and compute the center of that area.
+ if (prevLeft >= box.right) {
+ // The previously selected event was to the right of us.
+ prevLocation = FROM_RIGHT;
+ prevCenter = (prevTop + prevBottom) / 2;
+ } else if (prevRight <= box.left) {
+ // The previously selected event was to the left of us.
+ prevLocation = FROM_LEFT;
+ prevCenter = (prevTop + prevBottom) / 2;
+ } else if (prevBottom <= box.top) {
+ // The previously selected event was above us.
+ prevLocation = FROM_ABOVE;
+ prevCenter = (prevLeft + prevRight) / 2;
+ } else if (prevTop >= box.bottom) {
+ // The previously selected event was below us.
+ prevLocation = FROM_BELOW;
+ prevCenter = (prevLeft + prevRight) / 2;
+ }
+
+ // For each event in the selected event list "mSelectedEvents", search
+ // all the other events in that list for the nearest neighbor in 4
+ // directions.
+ for (int ii = 0; ii < len; ii++) {
+ Event ev = mSelectedEvents.get(ii);
+
+ int startTime = ev.startTime;
+ int endTime = ev.endTime;
+ int left = (int) ev.left;
+ int right = (int) ev.right;
+ int top = (int) ev.top;
+ if (top < box.top) {
+ top = box.top;
+ }
+ int bottom = (int) ev.bottom;
+ if (bottom > box.bottom) {
+ bottom = box.bottom;
+ }
+ if (false) {
+ int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
+ | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ String timeRange = DateUtils.formatDateRange(ev.startMillis, ev.endMillis,
+ flags);
+ Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
+ + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
+ }
+ int upDistanceMin = 10000; // any large number
+ int downDistanceMin = 10000; // any large number
+ int leftDistanceMin = 10000; // any large number
+ int rightDistanceMin = 10000; // any large number
+ Event upEvent = null;
+ Event downEvent = null;
+ Event leftEvent = null;
+ Event rightEvent = null;
+
+ // Pick the starting event closest to the previously selected event,
+ // if any. distance1 takes precedence over distance2.
+ int distance1 = 0;
+ int distance2 = 0;
+ if (prevLocation == FROM_ABOVE) {
+ if (left >= prevCenter) {
+ distance1 = left - prevCenter;
+ } else if (right <= prevCenter) {
+ distance1 = prevCenter - right;
+ }
+ distance2 = top - prevBottom;
+ } else if (prevLocation == FROM_BELOW) {
+ if (left >= prevCenter) {
+ distance1 = left - prevCenter;
+ } else if (right <= prevCenter) {
+ distance1 = prevCenter - right;
+ }
+ distance2 = prevTop - bottom;
+ } else if (prevLocation == FROM_LEFT) {
+ if (bottom <= prevCenter) {
+ distance1 = prevCenter - bottom;
+ } else if (top >= prevCenter) {
+ distance1 = top - prevCenter;
+ }
+ distance2 = left - prevRight;
+ } else if (prevLocation == FROM_RIGHT) {
+ if (bottom <= prevCenter) {
+ distance1 = prevCenter - bottom;
+ } else if (top >= prevCenter) {
+ distance1 = top - prevCenter;
+ }
+ distance2 = prevLeft - right;
+ }
+ if (distance1 < startEventDistance1
+ || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
+ startEvent = ev;
+ startEventDistance1 = distance1;
+ startEventDistance2 = distance2;
+ }
+
+ // For each neighbor, figure out if it is above or below or left
+ // or right of me and compute the distance.
+ for (int jj = 0; jj < len; jj++) {
+ if (jj == ii) {
+ continue;
+ }
+ Event neighbor = mSelectedEvents.get(jj);
+ int neighborLeft = (int) neighbor.left;
+ int neighborRight = (int) neighbor.right;
+ if (neighbor.endTime <= startTime) {
+ // This neighbor is entirely above me.
+ // If we overlap the same column, then compute the distance.
+ if (neighborLeft < right && neighborRight > left) {
+ int distance = startTime - neighbor.endTime;
+ if (distance < upDistanceMin) {
+ upDistanceMin = distance;
+ upEvent = neighbor;
+ } else if (distance == upDistanceMin) {
+ int center = (left + right) / 2;
+ int currentDistance = 0;
+ int currentLeft = (int) upEvent.left;
+ int currentRight = (int) upEvent.right;
+ if (currentRight <= center) {
+ currentDistance = center - currentRight;
+ } else if (currentLeft >= center) {
+ currentDistance = currentLeft - center;
+ }
+
+ int neighborDistance = 0;
+ if (neighborRight <= center) {
+ neighborDistance = center - neighborRight;
+ } else if (neighborLeft >= center) {
+ neighborDistance = neighborLeft - center;
+ }
+ if (neighborDistance < currentDistance) {
+ upDistanceMin = distance;
+ upEvent = neighbor;
+ }
+ }
+ }
+ } else if (neighbor.startTime >= endTime) {
+ // This neighbor is entirely below me.
+ // If we overlap the same column, then compute the distance.
+ if (neighborLeft < right && neighborRight > left) {
+ int distance = neighbor.startTime - endTime;
+ if (distance < downDistanceMin) {
+ downDistanceMin = distance;
+ downEvent = neighbor;
+ } else if (distance == downDistanceMin) {
+ int center = (left + right) / 2;
+ int currentDistance = 0;
+ int currentLeft = (int) downEvent.left;
+ int currentRight = (int) downEvent.right;
+ if (currentRight <= center) {
+ currentDistance = center - currentRight;
+ } else if (currentLeft >= center) {
+ currentDistance = currentLeft - center;
+ }
+
+ int neighborDistance = 0;
+ if (neighborRight <= center) {
+ neighborDistance = center - neighborRight;
+ } else if (neighborLeft >= center) {
+ neighborDistance = neighborLeft - center;
+ }
+ if (neighborDistance < currentDistance) {
+ downDistanceMin = distance;
+ downEvent = neighbor;
+ }
+ }
+ }
+ }
+
+ if (neighborLeft >= right) {
+ // This neighbor is entirely to the right of me.
+ // Take the closest neighbor in the y direction.
+ int center = (top + bottom) / 2;
+ int distance = 0;
+ int neighborBottom = (int) neighbor.bottom;
+ int neighborTop = (int) neighbor.top;
+ if (neighborBottom <= center) {
+ distance = center - neighborBottom;
+ } else if (neighborTop >= center) {
+ distance = neighborTop - center;
+ }
+ if (distance < rightDistanceMin) {
+ rightDistanceMin = distance;
+ rightEvent = neighbor;
+ } else if (distance == rightDistanceMin) {
+ // Pick the closest in the x direction
+ int neighborDistance = neighborLeft - right;
+ int currentDistance = (int) rightEvent.left - right;
+ if (neighborDistance < currentDistance) {
+ rightDistanceMin = distance;
+ rightEvent = neighbor;
+ }
+ }
+ } else if (neighborRight <= left) {
+ // This neighbor is entirely to the left of me.
+ // Take the closest neighbor in the y direction.
+ int center = (top + bottom) / 2;
+ int distance = 0;
+ int neighborBottom = (int) neighbor.bottom;
+ int neighborTop = (int) neighbor.top;
+ if (neighborBottom <= center) {
+ distance = center - neighborBottom;
+ } else if (neighborTop >= center) {
+ distance = neighborTop - center;
+ }
+ if (distance < leftDistanceMin) {
+ leftDistanceMin = distance;
+ leftEvent = neighbor;
+ } else if (distance == leftDistanceMin) {
+ // Pick the closest in the x direction
+ int neighborDistance = left - neighborRight;
+ int currentDistance = left - (int) leftEvent.right;
+ if (neighborDistance < currentDistance) {
+ leftDistanceMin = distance;
+ leftEvent = neighbor;
+ }
+ }
+ }
+ }
+ ev.nextUp = upEvent;
+ ev.nextDown = downEvent;
+ ev.nextLeft = leftEvent;
+ ev.nextRight = rightEvent;
+ }
+ mSelectedEvent = startEvent;
+ }
+
+
+ private RectF drawEventRect(Event event, Canvas canvas, Paint p) {
+ Drawable box = mBoxNormal;
+
+ // If this event is selected, then use the selection color
+ if (mSelectedEvent == event) {
+ if (mSelectionMode == SELECTION_PRESSED) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ box = mBoxPressed;
+ } else if (mSelectionMode == SELECTION_SELECTED) {
+ // Also, remember the last selected event that we drew
+ mPrevSelectedEvent = event;
+ box = mBoxSelected;
+ } else if (mSelectionMode == SELECTION_LONGPRESS) {
+ box = mBoxLongPressed;
+ }
+ }
+
+ RectF rf = mRectF;
+ rf.top = event.top;
+ rf.bottom = event.bottom;
+ rf.left = event.left;
+ rf.right = event.right;
+ int boxTop = (int) event.top;
+ int boxBottom = (int) event.bottom;
+ int boxLeft = (int) event.left;
+ int boxRight = (int) event.right;
+
+ box.setBounds(boxLeft, boxTop, boxRight, boxBottom);
+ box.draw(canvas);
+
+ // Save the coordinates
+ float eventRight = rf.right;
+ float eventTop = rf.top;
+ float eventBottom = rf.bottom;
+
+ // Draw the calendar color as a small rectangle on top of the event
+ // rectangle. Use a fixed size width unless it doesn't fit, in which
+ // case use 1/2 the width. For the height, use a fixed offset from
+ // the top and bottom unless that would be too small, in which case,
+ // use a 5% offset for top and bottom.
+ float width = CALENDAR_COLOR_WIDTH;
+ float maxWidth = (rf.right - rf.left) / 2.0f;
+ if (width > maxWidth) {
+ width = maxWidth;
+ }
+
+ // The drawable has a 1-pixel border so we need to shift the
+ // inner colored rectangle by one pixel. But we don't shift by 1
+ // if the rectangle is really small.
+ if (width >= 3) {
+ rf.left += 1;
+ }
+ float top = rf.top + CALENDAR_COLOR_HEIGHT_OFFSET;
+ float bottom = rf.bottom - CALENDAR_COLOR_HEIGHT_OFFSET;
+ float height = bottom - top;
+ if (height < MIN_EVENT_HEIGHT) {
+ float eventHeight = rf.bottom - rf.top;
+ top = rf.top + 0.2f * eventHeight;
+ bottom = rf.bottom - 0.2f * eventHeight;
+ }
+ rf.right = rf.left + width;
+ rf.top = top;
+ rf.bottom = bottom;
+ p.setColor(event.color);
+ canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
+
+ // Set the rectangle for the event text.
+ rf.left = rf.right;
+ rf.right = eventRight;
+ rf.top = eventTop;
+ rf.bottom = eventBottom;
+ return rf;
+ }
+
+ private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
+ if (mDrawTextInEventRect == false) {
+ return;
+ }
+
+ float width = rf.right - rf.left;
+ float height = rf.bottom - rf.top;
+
+ // Leave one pixel extra space between lines
+ int lineHeight = mEventTextHeight + 1;
+
+ // If the rectangle is too small for text, then return
+ if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
+ return;
+ }
+
+ // Truncate the event title to a known (large enough) limit
+ String text = event.getTitleAndLocation();
+ int len = text.length();
+ if (len > MAX_EVENT_TEXT_LEN) {
+ text = text.substring(0, MAX_EVENT_TEXT_LEN);
+ len = MAX_EVENT_TEXT_LEN;
+ }
+
+ // Figure out how much space the event title will take, and create a
+ // String fragment that will fit in the rectangle. Use multiple lines,
+ // if available.
+ p.getTextWidths(text, mCharWidths);
+ String fragment = text;
+ float top = rf.top + mEventTextAscent + topMargin;
+ int start = 0;
+
+ // Leave one pixel extra space at the bottom
+ while (start < len && height >= (lineHeight + 1)) {
+ boolean lastLine = (height < 2 * lineHeight + 1);
+ // Skip leading spaces at the beginning of each line
+ do {
+ char c = text.charAt(start);
+ if (c != ' ') break;
+ start += 1;
+ } while (start < len);
+
+ float sum = 0;
+ int end = start;
+ for (int ii = start; ii < len; ii++) {
+ char c = text.charAt(ii);
+
+ // If we found the end of a word, then remember the ending
+ // position.
+ if (c == ' ') {
+ end = ii;
+ }
+ sum += mCharWidths[ii];
+ // If adding this character would exceed the width and this
+ // isn't the last line, then break the line at the previous
+ // word. If there was no previous word, then break this word.
+ if (sum > width) {
+ if (end > start && !lastLine) {
+ // There was a previous word on this line.
+ fragment = text.substring(start, end);
+ start = end;
+ break;
+ }
+
+ // This is the only word and it is too long to fit on
+ // the line (or this is the last line), so take as many
+ // characters of this word as will fit.
+ fragment = text.substring(start, ii);
+ start = ii;
+ break;
+ }
+ }
+
+ // If sum <= width, then we can fit the rest of the text on
+ // this line.
+ if (sum <= width) {
+ fragment = text.substring(start, len);
+ start = len;
+ }
+
+ canvas.drawText(fragment, rf.left + 1, top, p);
+
+ top += lineHeight;
+ height -= lineHeight;
+ }
+ }
+
+ private void updateEventDetails() {
+ if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
+ || mSelectionMode == SELECTION_LONGPRESS) {
+ mPopup.dismiss();
+ return;
+ }
+
+ // Remove any outstanding callbacks to dismiss the popup.
+ getHandler().removeCallbacks(mDismissPopup);
+
+ Event event = mSelectedEvent;
+ TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
+ titleView.setText(event.title);
+
+ ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
+ imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
+
+ imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
+ imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
+
+ int flags;
+ if (event.allDay) {
+ flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
+ } else {
+ flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
+ | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+ }
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ String timeRange = DateUtils.formatDateRange(event.startMillis, event.endMillis, flags);
+ TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
+ timeView.setText(timeRange);
+
+ TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
+ whereView.setText(event.location);
+
+ mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
+ postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
+ }
+
+ // The following routines are called from the parent activity when certain
+ // touch events occur.
+
+ void doDown(MotionEvent ev) {
+ mTouchMode = TOUCH_MODE_DOWN;
+ mViewStartX = 0;
+ mOnFlingCalled = false;
+ mLaunchNewView = false;
+ getHandler().removeCallbacks(mContinueScroll);
+ }
+
+ void doSingleTapUp(MotionEvent ev) {
+ mSelectionMode = SELECTION_SELECTED;
+ mRedrawScreen = true;
+ invalidate();
+ if (mLaunchNewView) {
+ mLaunchNewView = false;
+ switchViews(false /* not the trackball */);
+ }
+ }
+
+ void doShowPress(MotionEvent ev) {
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ Event selectedEvent = mSelectedEvent;
+ int selectedDay = mSelectionDay;
+ int selectedHour = mSelectionHour;
+
+ boolean validPosition = setSelectionFromPosition(x, y);
+ if (!validPosition) {
+ return;
+ }
+
+ mSelectionMode = SELECTION_PRESSED;
+ mRedrawScreen = true;
+ invalidate();
+
+ // If the tap is on an already selected event or hour slot,
+ // then launch a new view. Otherwise, just select the event.
+ if (selectedEvent != null && selectedEvent == mSelectedEvent) {
+ // Launch the "View event" view when the finger lifts up,
+ // unless the finger moves before lifting up.
+ mLaunchNewView = true;
+ } else if (selectedEvent == null && selectedDay == mSelectionDay
+ && selectedHour == mSelectionHour) {
+ // Launch the Day/Agenda view when the finger lifts up,
+ // unless the finger moves before lifting up.
+ mLaunchNewView = true;
+ }
+ }
+
+ void doLongPress(MotionEvent ev) {
+ mLaunchNewView = false;
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ performLongClick();
+ }
+
+ void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
+ mLaunchNewView = false;
+ // Use the distance from the current point to the initial touch instead
+ // of deltaX and deltaY to avoid accumulating floating-point rounding
+ // errors. Also, we don't need floats, we can use ints.
+ int distanceX = (int) e1.getX() - (int) e2.getX();
+ int distanceY = (int) e1.getY() - (int) e2.getY();
+
+ // If we haven't figured out the predominant scroll direction yet,
+ // then do it now.
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ int absDistanceX = Math.abs(distanceX);
+ int absDistanceY = Math.abs(distanceY);
+ mScrollStartY = mViewStartY;
+ mPreviousDistanceX = 0;
+ mPreviousDirection = 0;
+
+ // If the x distance is at least twice the y distance, then lock
+ // the scroll horizontally. Otherwise scroll vertically.
+ if (absDistanceX >= 2 * absDistanceY) {
+ mTouchMode = TOUCH_MODE_HSCROLL;
+ mViewStartX = distanceX;
+ initNextView(-mViewStartX);
+ } else {
+ mTouchMode = TOUCH_MODE_VSCROLL;
+ }
+ } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ // We are already scrolling horizontally, so check if we
+ // changed the direction of scrolling so that the other week
+ // is now visible.
+ mViewStartX = distanceX;
+ if (distanceX != 0) {
+ int direction = (distanceX > 0) ? 1 : -1;
+ if (direction != mPreviousDirection) {
+ // The user has switched the direction of scrolling
+ // so re-init the next view
+ initNextView(-mViewStartX);
+ mPreviousDirection = direction;
+ }
+ }
+
+ // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
+ // then change the title to the new day (or week), but only
+ // if we haven't already changed the title.
+ if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
+ if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ }
+ } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
+ if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ }
+ } else {
+ if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
+ || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
+ mTitleTextView.setText(mDateRange);
+ }
+ }
+ mPreviousDistanceX = distanceX;
+ }
+
+ if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
+ mViewStartY = mScrollStartY + distanceY;
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY;
+ }
+ computeFirstHour();
+ }
+
+ mScrolling = true;
+
+ if (mSelectionMode != SELECTION_HIDDEN) {
+ mSelectionMode = SELECTION_HIDDEN;
+ mRedrawScreen = true;
+ }
+ invalidate();
+ }
+
+ void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ mTouchMode = TOUCH_MODE_INITIAL_STATE;
+ mSelectionMode = SELECTION_HIDDEN;
+ mOnFlingCalled = true;
+ int deltaX = (int) e2.getX() - (int) e1.getX();
+ int distanceX = Math.abs(deltaX);
+ int deltaY = (int) e2.getY() - (int) e1.getY();
+ int distanceY = Math.abs(deltaY);
+
+ if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
+ boolean switchForward = initNextView(deltaX);
+ CalendarView view = mParentActivity.getNextView();
+ mTitleTextView.setText(view.mDateRange);
+ mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
+ mViewStartX = 0;
+ return;
+ }
+
+ // Continue scrolling vertically
+ mContinueScroll.init((int) velocityY / 20);
+ post(mContinueScroll);
+ }
+
+ private boolean initNextView(int deltaX) {
+ // Change the view to the previous day or week
+ CalendarView view = mParentActivity.getNextView();
+ Time date = view.mBaseDate;
+ date.set(mBaseDate);
+ int selectionDay;
+ boolean switchForward;
+ if (deltaX > 0) {
+ date.monthDay -= mNumDays;
+ view.mSelectionDay = mSelectionDay - mNumDays;
+ switchForward = false;
+ } else {
+ date.monthDay += mNumDays;
+ view.mSelectionDay = mSelectionDay + mNumDays;
+ switchForward = true;
+ }
+ date.normalize(true /* ignore isDst */);
+ initView(view);
+ view.setFrame(getLeft(), getTop(), getRight(), getBottom());
+ view.reloadEvents();
+ return switchForward;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ if (mOnFlingCalled) {
+ return true;
+ }
+ if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
+ mTouchMode = TOUCH_MODE_INITIAL_STATE;
+ if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
+ // The user has gone beyond the threshold so switch views
+ mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
+ } else {
+ // Not beyond the threshold so invalidate which will cause
+ // the view to snap back. Also call recalc() to ensure
+ // that we have the correct starting date and title.
+ recalc();
+ mTitleTextView.setText(mDateRange);
+ invalidate();
+ }
+ mViewStartX = 0;
+ }
+
+ // If we were scrolling, then reset the selected hour so that it
+ // is visible.
+ if (mScrolling) {
+ mScrolling = false;
+ resetSelectedHour();
+ mRedrawScreen = true;
+ invalidate();
+ }
+ return true;
+
+ // This case isn't expected to happen.
+ case MotionEvent.ACTION_CANCEL:
+ mParentActivity.mGestureDetector.onTouchEvent(ev);
+ mScrolling = false;
+ resetSelectedHour();
+ return true;
+
+ default:
+ if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+ MenuItem item;
+
+ // If the trackball is held down, then the context menu pops up and
+ // we never get onKeyUp() for the long-press. So check for it here
+ // and change the selection to the long-press state.
+ if (mSelectionMode != SELECTION_LONGPRESS) {
+ mSelectionMode = SELECTION_LONGPRESS;
+ mRedrawScreen = true;
+ invalidate();
+ }
+
+ int numSelectedEvents = mSelectedEvents.size();
+ if (mNumDays == 1) {
+ // Day view.
+ // If there is a selected event, then allow it to be viewed and
+ // edited.
+ if (numSelectedEvents >= 1) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_info_details);
+
+ if (isEventEditable(mContext, mSelectedEvent)) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_edit);
+ item.setAlphabeticShortcut('e');
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+ } else {
+ // Otherwise, if the user long-pressed on a blank hour, allow
+ // them to create an event. They can also do this by tapping.
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+ }
+ } else {
+ // Week view.
+ // If there is a selected event, then allow it to be viewed and
+ // edited.
+ if (numSelectedEvents >= 1) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_info_details);
+
+ if (isEventEditable(mContext, mSelectedEvent)) {
+ item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_edit);
+ item.setAlphabeticShortcut('e');
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.day_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.agenda_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+ } else {
+ // No events are selected
+ item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_add);
+ item.setAlphabeticShortcut('n');
+
+ item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.day_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_day);
+ item.setAlphabeticShortcut('d');
+
+ item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.agenda_view);
+ item.setOnMenuItemClickListener(mContextMenuHandler);
+ item.setIcon(android.R.drawable.ic_menu_agenda);
+ item.setAlphabeticShortcut('a');
+ }
+ }
+
+ mPopup.dismiss();
+ }
+
+ private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case MenuHelper.MENU_EVENT_VIEW: {
+ if (mSelectedEvent != null) {
+ long id = mSelectedEvent.id;
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EventInfoActivity.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ break;
+ }
+ case MenuHelper.MENU_EVENT_EDIT: {
+ if (mSelectedEvent != null) {
+ long id = mSelectedEvent.id;
+ Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ intent.setData(eventUri);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
+ intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
+ mParentActivity.startActivity(intent);
+ }
+ break;
+ }
+ case MenuHelper.MENU_DAY: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, DayActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_AGENDA: {
+ long startMillis = getSelectedTimeInMillis();
+ MenuHelper.switchTo(mParentActivity, AgendaActivity.class.getName(), startMillis);
+ mParentActivity.finish();
+ break;
+ }
+ case MenuHelper.MENU_EVENT_CREATE: {
+ long startMillis = getSelectedTimeInMillis();
+ long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setClassName(mContext, EditEvent.class.getName());
+ intent.putExtra(EVENT_BEGIN_TIME, startMillis);
+ intent.putExtra(EVENT_END_TIME, endMillis);
+ intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
+ mParentActivity.startActivity(intent);
+ break;
+ }
+ case MenuHelper.MENU_EVENT_DELETE: {
+ if (mSelectedEvent != null) {
+ Event selectedEvent = mSelectedEvent;
+ long begin = selectedEvent.startMillis;
+ long end = selectedEvent.endMillis;
+ long id = selectedEvent.id;
+ mDeleteEventHelper.delete(begin, end, id, -1);
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static boolean isEventEditable(Context context, Event e) {
+ ContentResolver cr = context.getContentResolver();
+
+ int visibility = Calendars.NO_ACCESS;
+ int relationship = Attendees.RELATIONSHIP_ORGANIZER;
+
+ // Get the calendar id for this event
+ Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
+ new String[] { Events.CALENDAR_ID },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if ((cursor == null) || (cursor.getCount() == 0)) {
+ return false;
+ }
+ cursor.moveToFirst();
+ long calId = cursor.getLong(0);
+ cursor.deactivate();
+
+ Uri uri = Calendars.CONTENT_URI;
+ String where = String.format(CALENDARS_WHERE, calId);
+ cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
+
+ if (cursor != null) {
+ cursor.moveToFirst();
+ visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
+ cursor.close();
+ }
+
+ // Attendees cursor
+ uri = Attendees.CONTENT_URI;
+ where = String.format(ATTENDEES_WHERE, e.id);
+ Cursor attendeesCursor = cr.query(uri, ATTENDEES_PROJECTION, where, null, null);
+ if (attendeesCursor != null) {
+ if (attendeesCursor.moveToFirst()) {
+ relationship = attendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+ }
+ }
+ attendeesCursor.close();
+
+ if (visibility >= Calendars.CONTRIBUTOR_ACCESS &&
+ relationship >= Attendees.RELATIONSHIP_ORGANIZER) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
+ * If the touch position is not within the displayed grid, then this
+ * method returns false.
+ *
+ * @param x the x position of the touch
+ * @param y the y position of the touch
+ * @return true if the touch position is valid
+ */
+ private boolean setSelectionFromPosition(int x, int y) {
+ if (x < mHoursWidth) {
+ return false;
+ }
+
+ int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
+ if (day >= mNumDays) {
+ day = mNumDays - 1;
+ }
+ day += mFirstJulianDay;
+ int hour;
+ if (y < mFirstCell + mFirstHourOffset) {
+ mSelectionAllDay = true;
+ } else {
+ hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
+ hour += mFirstHour;
+ mSelectionHour = hour;
+ mSelectionAllDay = false;
+ }
+ mSelectionDay = day;
+ findSelectedEvent(x, y);
+// Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
+// + " hour: " + hour
+// + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
+// if (mSelectedEvent != null) {
+// Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
+// for (Event ev : mSelectedEvents) {
+// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
+// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
+// String timeRange = formatDateRange(mResources, ev.startMillis, ev.endMillis, flags);
+//
+// Log.i("Cal", " " + timeRange + " " + ev.title);
+// }
+// }
+ return true;
+ }
+
+ private void findSelectedEvent(int x, int y) {
+ int date = mSelectionDay;
+ int cellWidth = mCellWidth;
+ ArrayList<Event> events = mEvents;
+ int numEvents = events.size();
+ int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
+ int top = 0;
+ mSelectedEvent = null;
+
+ mSelectedEvents.clear();
+ if (mSelectionAllDay) {
+ float yDistance;
+ float minYdistance = 10000.0f; // any large number
+ Event closestEvent = null;
+ float drawHeight = mAllDayHeight;
+ int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ if (!event.allDay) {
+ continue;
+ }
+
+ if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
+ float numRectangles = event.getMaxColumns();
+ float height = drawHeight / numRectangles;
+ if (height > MAX_ALLDAY_EVENT_HEIGHT) {
+ height = MAX_ALLDAY_EVENT_HEIGHT;
+ }
+ float eventTop = yOffset + height * event.getColumn();
+ float eventBottom = eventTop + height;
+ if (eventTop < y && eventBottom > y) {
+ // If the touch is inside the event rectangle, then
+ // add the event.
+ mSelectedEvents.add(event);
+ closestEvent = event;
+ break;
+ } else {
+ // Find the closest event
+ if (eventTop >= y) {
+ yDistance = eventTop - y;
+ } else {
+ yDistance = y - eventBottom;
+ }
+ if (yDistance < minYdistance) {
+ minYdistance = yDistance;
+ closestEvent = event;
+ }
+ }
+ }
+ }
+ mSelectedEvent = closestEvent;
+ return;
+ }
+
+ // Adjust y for the scrollable bitmap
+ y += mViewStartY - mFirstCell;
+
+ // Use a region around (x,y) for the selection region
+ Rect region = mRect;
+ region.left = x - 10;
+ region.right = x + 10;
+ region.top = y - 10;
+ region.bottom = y + 10;
+
+ EventGeometry geometry = mEventGeometry;
+
+ for (int i = 0; i < numEvents; i++) {
+ Event event = events.get(i);
+ // Compute the event rectangle.
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
+ continue;
+ }
+
+ // If the event intersects the selection region, then add it to
+ // mSelectedEvents.
+ if (geometry.eventIntersectsSelection(event, region)) {
+ mSelectedEvents.add(event);
+ }
+ }
+
+ // If there are any events in the selected region, then assign the
+ // closest one to mSelectedEvent.
+ if (mSelectedEvents.size() > 0) {
+ int len = mSelectedEvents.size();
+ Event closestEvent = null;
+ float minDist = mViewWidth + mViewHeight; // some large distance
+ for (int index = 0; index < len; index++) {
+ Event ev = mSelectedEvents.get(index);
+ float dist = geometry.pointToEvent(x, y, ev);
+ if (dist < minDist) {
+ minDist = dist;
+ closestEvent = ev;
+ }
+ }
+ mSelectedEvent = closestEvent;
+
+ // Keep the selected hour and day consistent with the selected
+ // event. They could be different if we touched on an empty hour
+ // slot very close to an event in the previous hour slot. In
+ // that case we will select the nearby event.
+ int startDay = mSelectedEvent.startDay;
+ int endDay = mSelectedEvent.endDay;
+ if (mSelectionDay < startDay) {
+ mSelectionDay = startDay;
+ } else if (mSelectionDay > endDay) {
+ mSelectionDay = endDay;
+ }
+
+ int startHour = mSelectedEvent.startTime / 60;
+ int endHour;
+ if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
+ endHour = (mSelectedEvent.endTime - 1) / 60;
+ } else {
+ endHour = mSelectedEvent.endTime / 60;
+ }
+
+ if (mSelectionHour < startHour) {
+ mSelectionHour = startHour;
+ } else if (mSelectionHour > endHour) {
+ mSelectionHour = endHour;
+ }
+ }
+ }
+
+ // Encapsulates the code to continue the scrolling after the
+ // finger is lifted. Instead of stopping the scroll immediately,
+ // the scroll continues to "free spin" and gradually slows down.
+ private class ContinueScroll implements Runnable {
+ int mSignDeltaY;
+ int mAbsDeltaY;
+ float mFloatDeltaY;
+ long mFreeSpinTime;
+ private static final float FRICTION_COEF = 0.7F;
+ private static final long FREE_SPIN_MILLIS = 180;
+ private static final int MAX_DELTA = 60;
+ private static final int SCROLL_REPEAT_INTERVAL = 30;
+
+ public void init(int deltaY) {
+ mSignDeltaY = 0;
+ if (deltaY > 0) {
+ mSignDeltaY = 1;
+ } else if (deltaY < 0) {
+ mSignDeltaY = -1;
+ }
+ mAbsDeltaY = Math.abs(deltaY);
+
+ // Limit the maximum speed
+ if (mAbsDeltaY > MAX_DELTA) {
+ mAbsDeltaY = MAX_DELTA;
+ }
+ mFloatDeltaY = mAbsDeltaY;
+ mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
+// Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
+// + " mViewStartY: " + mViewStartY);
+ }
+
+ public void run() {
+ long time = System.currentTimeMillis();
+
+ // Start out with a frictionless "free spin"
+ if (time > mFreeSpinTime) {
+ // If the delta is small, then apply a fixed deceleration.
+ // Otherwise
+ if (mAbsDeltaY <= 10) {
+ mAbsDeltaY -= 2;
+ } else {
+ mFloatDeltaY *= FRICTION_COEF;
+ mAbsDeltaY = (int) mFloatDeltaY;
+ }
+
+ if (mAbsDeltaY < 0) {
+ mAbsDeltaY = 0;
+ }
+ }
+
+ if (mSignDeltaY == 1) {
+ mViewStartY -= mAbsDeltaY;
+ } else {
+ mViewStartY += mAbsDeltaY;
+ }
+// Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
+// + " mViewStartY: " + mViewStartY);
+
+ if (mViewStartY < 0) {
+ mViewStartY = 0;
+ mAbsDeltaY = 0;
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY;
+ mAbsDeltaY = 0;
+ }
+
+ computeFirstHour();
+
+ if (mAbsDeltaY > 0) {
+ postDelayed(this, SCROLL_REPEAT_INTERVAL);
+ } else {
+ // Done scrolling.
+ mScrolling = false;
+ resetSelectedHour();
+ mRedrawScreen = true;
+ }
+
+ invalidate();
+ }
+ }
+
+ /**
+ * Cleanup the pop-up.
+ */
+ public void cleanup() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mDismissPopup);
+ }
+ }
+
+ @Override protected void onDetachedFromWindow() {
+ cleanup();
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ super.onDetachedFromWindow();
+ }
+
+ class DismissPopup implements Runnable {
+ public void run() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+ }
+ }
+}
+