在好例子网,分享、交流、成长!
您当前所在位置:首页Java 开发实例Android平台开发 → 安卓日历源码下载

安卓日历源码下载

Android平台开发

下载此实例
  • 开发语言:Java
  • 实例大小:2.97M
  • 下载次数:86
  • 浏览次数:2050
  • 发布时间:2014-12-24
  • 实例类别:Android平台开发
  • 发 布 人:喀秋莎
  • 文件格式:.zip
  • 所需积分:2
 相关标签: 日历 安卓

实例介绍

【实例简介】android 日历

【实例截图】

【核心代码】


package com.yuanma.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.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
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.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 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 static float mScale = 0; // Used for supporting different screen densities
    private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event

    private boolean mOnFlingCalled;
    /**
     * ID of the last event which was displayed with the toast popup.
     *
     * This is used to prevent popping up multiple quick views for the same event, especially
     * during calendar syncs. This becomes valid when an event is selected, either by default
     * on starting calendar or by scrolling to an event. It becomes invalid when the user
     * explicitly scrolls to an empty time slot, changes views, or deletes the event.
     */
    private long mLastPopupEventID;

    protected CalendarApplication mCalendarApp;
    protected CalendarActivity mParentActivity;

    // This runs when we need to update the tz
    private Runnable mUpdateTZ = new Runnable() {
        @Override
        public void run() {
            String tz = Utils.getTimeZone(mContext, this);
            // BaseDate we want to keep on the same day, so we swap tz
            mBaseDate.timezone = tz;
            mBaseDate.normalize(true);
            // CurrentTime we want to keep at the same absolute time, so we
            // call switch tz
            mCurrentTime.switchTimezone(tz);
            mTimeZone = TimeZone.getTimeZone(tz);
            recalc();
            mTitleTextView.setText(mDateRange);
        }
    };
    private Context mContext;

    private static final String[] CALENDARS_PROJECTION = new String[] {
        Calendars._ID,          // 0
        Calendars.ACCESS_LEVEL, // 1
        Calendars.OWNER_ACCOUNT, // 2
    };
    private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
    private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
    private static final String CALENDARS_WHERE = Calendars._ID   "=?";

    private static 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 ACCESS_LEVEL_NONE = 0;
    private static final int ACCESS_LEVEL_DELETE = 1;
    private static final int ACCESS_LEVEL_EDIT = 2;

    private static int HORIZONTAL_SCROLL_THRESHOLD = 50;

    private ContinueScroll mContinueScroll = new ContinueScroll();

    static private class DayHeader{
        int cell;
        String dateString;
    }

    private DayHeader[] dayHeaders = new DayHeader[32];

    // Make this visible within the package for more informative debugging
    Time mBaseDate;
    private Time mCurrentTime;
    //Update the current time line every five minutes if the window is left open that long
    private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
    private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
    private int mTodayJulianDay;

    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 int mSelectionDay;        // Julian day
    private int mSelectionHour;

    /* package private so that CalendarActivity can read it when creating new
     * events
     */
    boolean mSelectionAllDay;

    private int mCellWidth;

    // 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 mPaintBorder = new Paint();
    private Paint mEventTextPaint = new Paint();
    private Paint mSelectionPaint = new Paint();
    private Path mPath = new Path();

    protected boolean mDrawTextInEventRect;
    private int mStartDay;

    private PopupWindow mPopup;
    private View mPopupView;

    // 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 int SINGLE_ALLDAY_HEIGHT = 20;
    private static int MAX_ALLDAY_HEIGHT = 72;
    private static int ALLDAY_TOP_MARGIN = 3;
    private static 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;

    private static int CURRENT_TIME_LINE_HEIGHT = 2;
    private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
    private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6;
    private static int CURRENT_TIME_MARKER_HEIGHT = 6;
    private static int CURRENT_TIME_MARKER_WIDTH = 8;
    private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1;

    /* 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 int NORMAL_FONT_SIZE = 12;
    private static int EVENT_TEXT_FONT_SIZE = 12;
    private static int HOURS_FONT_SIZE = 12;
    private static int AMPM_FONT_SIZE = 9;
    private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
    private static final int MAX_EVENT_TEXT_LEN = 500;
    private static float MIN_EVENT_HEIGHT = 15.0F;  // in pixels

    // This value forces the position calculator to take care of the overwap which can't be
    // detected from the view of event time but actually is detected when rendering them.
    //
    // Detail:
    // Imagine there are two events: A (from 1:00pm to 1:01pm) and B (from 1:02pm to 2:00pm).
    // The position calculator (Event#doComputePositions()), marks them as "not overwrapped"
    // as A finishes before B's begin time, so those events are put on the same column
    // (or, horizontal position).
    // From the view of renderer, however, the actual rectangle for A is larger than "1 min."
    // for accomodating at least 1 line of text in it.
    // As a result, A's rectangle is overwrapped by B's, and A becomes hard to be touched
    // without trackball or DPAD (as, it is beneath B from the user' view).
    // This values forces the original calculator to take care of the actual overwrap detected in
    // rendering time.
    //
    // Note:
    // Theoretically we can calcurate an ideal value for this purpose by making the calculator
    // understand the relation between each event and pixel-level height of actual rectangles,
    // but we don't do so as currently the calculator doesn't have convenient way to obtain
    // necessary values for the calculation.
    /* package */ static long EVENT_OVERWRAP_MARGIN_TIME = MILLIS_PER_MINUTE * 15;

    private static int mSelectionColor;
    private static int mPressedColor;
    private static int mSelectedEventTextColor;
    private static int mEventTextColor;
    private static int mWeek_saturdayColor;
    private static int mWeek_sundayColor;
    private static int mCalendarDateBannerTextColor;
    private static int mCalendarAllDayBackground;
    private static int mCalendarAmPmLabel;
    private static int mCalendarDateBannerBackground;
    private static int mCalendarDateSelected;
    private static int mCalendarGridAreaBackground;
    private static int mCalendarGridAreaSelected;
    private static int mCalendarGridLineHorizontalColor;
    private static int mCalendarGridLineVerticalColor;
    private static int mCalendarHourBackground;
    private static int mCalendarHourLabel;
    private static int mCalendarHourSelected;
    private static int mCurrentTimeMarkerColor;
    private static int mCurrentTimeLineColor;
    private static int mCurrentTimeMarkerBorderColor;

    private int mViewStartX;
    private int mViewStartY;
    private int mMaxViewStartY;
    private int mBitmapHeight;
    private int mViewHeight;
    private int mViewWidth;
    private int mGridAreaHeight;
    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 TimeZone mTimeZone;
    private String mDateRange;
    private TextView mTitleTextView;

    // Accessibility support related members

    private int mPrevSelectionDay;
    private int mPrevSelectionHour;
    private CharSequence mPrevTitleTextViewText;
    private Bundle mTempEventBundle;

    public CalendarView(CalendarActivity activity) {
        super(activity);
        if (mScale == 0) {
            mScale = getContext().getResources().getDisplayMetrics().density;
            if (mScale != 1) {
                SINGLE_ALLDAY_HEIGHT *= mScale;
                MAX_ALLDAY_HEIGHT *= mScale;
                ALLDAY_TOP_MARGIN *= mScale;
                MAX_ALLDAY_EVENT_HEIGHT *= mScale;

                NORMAL_FONT_SIZE *= mScale;
                EVENT_TEXT_FONT_SIZE *= mScale;
                HOURS_FONT_SIZE *= mScale;
                AMPM_FONT_SIZE *= mScale;
                MIN_CELL_WIDTH_FOR_TEXT *= mScale;
                MIN_EVENT_HEIGHT *= mScale;

                HORIZONTAL_SCROLL_THRESHOLD *= mScale;

                CURRENT_TIME_MARKER_HEIGHT *= mScale;
                CURRENT_TIME_MARKER_WIDTH *= mScale;
                CURRENT_TIME_LINE_HEIGHT *= mScale;
                CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
                CURRENT_TIME_MARKER_INNER_WIDTH *= mScale;
                CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;

                SMALL_ROUND_RADIUS *= mScale;
            }
        }

        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 */);
        mLastPopupEventID = INVALID_EVENT_ID;

        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 = Utils.getFirstDayOfWeek();

        mTimeZone = TimeZone.getTimeZone(Utils.getTimeZone(context, mUpdateTZ));

        mContext = context;
        mCurrentTime = new Time(Utils.getTimeZone(context, mUpdateTZ));
        long currentTime = System.currentTimeMillis();
        mCurrentTime.set(currentTime);
        //The % makes it go off at the next increment of 5 minutes.
        postDelayed(mUpdateCurrentTime,
                UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
        mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);

        mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
        mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
        mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
        mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
        mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
        mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
        mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
        mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
        mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
        mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
        mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
        mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
        mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
        mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
        mSelectionColor = mResources.getColor(R.color.selection);
        mPressedColor = mResources.getColor(R.color.pressed);
        mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
        mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
        mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker);
        mCurrentTimeLineColor = mResources.getColor(R.color.current_time_line);
        mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border);
        mEventTextPaint.setColor(mEventTextColor);
        mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
        mEventTextPaint.setTextAlign(Paint.Align.LEFT);
        mEventTextPaint.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);

        mPaintBorder.setColor(0xffc8c8c8);
        mPaintBorder.setStyle(Style.STROKE);
        mPaintBorder.setAntiAlias(true);
        mPaintBorder.setStrokeWidth(2.0f);

        // 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);

            // If we don't have 2-letter day strings, fall back to 1-letter.
            if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
                mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
            }

            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);
        updateIs24HourFormat();

        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;

        LayoutInflater inflater;
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mPopupView = inflater.inflate(R.layout.bubble_event, null);
        mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        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(Utils.getTimeZone(context, mUpdateTZ));
        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 */);
        }
    }

    public void updateIs24HourFormat() {
        mIs24HourFormat = DateFormat.is24HourFormat(mParentActivity);
        mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
    }

    /**
     * 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 */);
            }
        }

        long start = mBaseDate.normalize(true /* 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(mParentActivity)) {
            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 = Utils.formatDateRange(mParentActivity, start, end, flags);

        if (!TextUtils.equals(Utils.getTimeZone(mContext, mUpdateTZ), Time.getCurrentTimezone())) {
            flags = DateUtils.FORMAT_SHOW_TIME;
            if (DateFormat.is24HourFormat(mParentActivity)) {
                flags |= DateUtils.FORMAT_24HOUR;
            }
            start = System.currentTimeMillis();

            String tz = Utils.getTimeZone(mContext, mUpdateTZ);
            boolean isDST = mBaseDate.isDst != 0;
            StringBuilder title = new StringBuilder(mDateRange);
            title.append(" (").append(Utils.formatDateRange(mContext, start, start, flags))
                    .append(" ")
                    .append(mTimeZone.getDisplayName(isDST, TimeZone.SHORT, Locale.getDefault()))
                    .append(")");
            mDateRange = title.toString();
        }
        // 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;
        int gridAreaWidth = width - mHoursWidth;
        mCellWidth = (gridAreaWidth - (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);
        //When we get new events we don't want to dismiss the popup unless the event changes
        if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
            mPopup.dismiss();
        }
        mPopup.setWidth(eventAreaWidth - 20);
        mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
    }

    /**
     * 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();
        mLastPopupEventID = INVALID_EVENT_ID;
        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(mParentActivity, 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(mParentActivity, 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(mParentActivity, 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();
                    Utils.startActivity(mParentActivity, mDetailedView, millis);
                }
            }
        } 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(mParentActivity, 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(mParentActivity, 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;
            case KeyEvent.KEYCODE_BACK:
                if (event.isTracking() && !event.isCanceled()) {
                    mPopup.dismiss();
                    mParentActivity.finish();
                    return true;
                }
                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;
        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();
            mLastPopupEventID = INVALID_EVENT_ID;

            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:
            if (event.getRepeatCount() == 0) {
                event.startTracking();
                return true;
            }
            return super.onKeyDown(keyCode, event);
        case KeyEvent.KEYCODE_DPAD_LEFT:
            if (mSelectedEvent != null) {
                mSelectedEvent = mSelectedEvent.nextLeft;
            }
            if (mSelectedEvent == null) {
                mLastPopupEventID = INVALID_EVENT_ID;
                selectionDay -= 1;
            }
            redraw = true;
            break;

        case KeyEvent.KEYCODE_DPAD_RIGHT:
            if (mSelectedEvent != null) {
                mSelectedEvent = mSelectedEvent.nextRight;
            }
            if (mSelectedEvent == null) {
                mLastPopupEventID = INVALID_EVENT_ID;
                selectionDay  = 1;
            }
            redraw = true;
            break;

        case KeyEvent.KEYCODE_DPAD_UP:
            if (mSelectedEvent != null) {
                mSelectedEvent = mSelectedEvent.nextUp;
            }
            if (mSelectedEvent == null) {
                mLastPopupEventID = INVALID_EVENT_ID;
                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) {
                mLastPopupEventID = INVALID_EVENT_ID;
                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(Utils.getTimeZone(mContext, mUpdateTZ));
        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();
        }

        sendAccessibilityEvents();
    }

    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(mCalendarHourBackground);
        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(mCalendarDateBannerBackground);
        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(mCalendarGridAreaBackground);
        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(mCalendarDateSelected);
                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;
        }

        p.setTypeface(mBold);
        p.setAntiAlias(true);
        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(mCalendarAmPmLabel);
        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 drawCurrentTimeMarker(int top, Canvas canvas, Paint p) {
        Rect r = new Rect();
        r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
        r.bottom = top   CURRENT_TIME_LINE_HEIGHT / 2;
        r.left = 0;
        r.right = mHoursWidth;

        p.setColor(mCurrentTimeMarkerColor);
        canvas.drawRect(r, p);
    }

    private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) {
        //Do a white outline so it'll show up on a red event
        p.setColor(mCurrentTimeMarkerBorderColor);
        r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH;
        r.bottom = top   CURRENT_TIME_LINE_HEIGHT / 2   CURRENT_TIME_LINE_BORDER_WIDTH;
        r.left = left   CURRENT_TIME_LINE_SIDE_BUFFER;
        r.right = r.left   mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER;
        canvas.drawRect(r, p);
        //Then draw the red line
        p.setColor(mCurrentTimeLineColor);
        r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
        r.bottom = top   CURRENT_TIME_LINE_HEIGHT / 2;
        canvas.drawRect(r, p);
    }

    private void doDraw(Canvas canvas) {
        Paint p = mPaint;
        Rect r = mRect;
        int lineY = mCurrentTime.hour*(mCellHeight   HOUR_GAP)
              ((mCurrentTime.minute * mCellHeight) / 60)
              1;

        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);
            //If this is today
            if(cell == mTodayJulianDay) {
                //And the current time shows up somewhere on the screen
                if(lineY >= mViewStartY && lineY < mViewStartY   mViewHeight - 2) {
                    //draw both the marker and the line
                    drawCurrentTimeMarker(lineY, canvas, p);
                    drawCurrentTimeLine(r, x, lineY, canvas, p);
                }
            }
            x  = deltaX;
        }
    }

    private void drawHours(Rect r, Canvas canvas, Paint p) {
        // Draw the background for the hour labels
        p.setColor(mCalendarHourBackground);
        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(mCalendarGridAreaBackground);
        canvas.drawRect(r, p);

        // Draw a highlight on the selected hour (if needed)
        if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
            p.setColor(mCalendarHourSelected);
            r.top = mSelectionHour * (mCellHeight   HOUR_GAP);
            r.bottom = r.top   mCellHeight   2 * HOUR_GAP;
            r.left = 0;
            r.right = mHoursWidth;
            canvas.drawRect(r, p);

            boolean drawBorder = false;
            if (!drawBorder) {
                r.top  = HOUR_GAP;
                r.bottom -= HOUR_GAP;
            }

            // Also draw the highlight on the grid
            p.setColor(mCalendarGridAreaSelected);
            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.
            if (drawBorder) {
                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(mCalendarHourLabel);
        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 sendAccessibilityEvents() {
//        if (!isShown() || !AccessibilityManager.getInstance(mContext).isEnabled()) {
//            return;
//        }
        // if the title text has changed => announce period
        CharSequence titleTextViewText = mTitleTextView.getText();
        // intended use of identity comparison
        boolean titleChanged = titleTextViewText != mPrevTitleTextViewText;
        if (titleChanged) {
            mPrevTitleTextViewText = titleTextViewText;
            sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
        }
        // if title or selection has changed => announce selection
        // Note: if the title has changed we want to send both events
        if (titleChanged || mPrevSelectionDay != mSelectionDay
                || mPrevSelectionHour != mSelectionHour) {
            mPrevSelectionDay = mSelectionDay;
            mPrevSelectionHour = mSelectionHour;
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }

    @Override
    public void sendAccessibilityEvent(int eventType) {
        // we send only selection events since semantically we select
        // certain element and not always this view gets focus which
        // triggers firing of a focus accessibility event
        if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
            return;
        }
        super.sendAccessibilityEvent(eventType);
    }

    @Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            // add the currently shown period (day/week)
            if (mNumDays == 1) {
                // for daily view the title has enough context information
                event.getText().add(mTitleTextView.getText());
            } else {
                // since the title view does not contain enough context we
                // compute a more descriptive title for the shown time frame
                int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH
                        | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
                if (DateFormat.is24HourFormat(mParentActivity)) {
                    flags |= DateUtils.FORMAT_24HOUR;
                }

                long start = mBaseDate.toMillis(false);
                long gmtOff = mBaseDate.gmtoff;
                int firstJulianDay = Time.getJulianDay(start, gmtOff);

                Time time = new Time(mBaseDate);
                time.setJulianDay(firstJulianDay);
                long startTime = time.normalize(true);
                time.setJulianDay(firstJulianDay   mNumDays);
                long endTime = time.normalize(true);

                String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
                        flags);
                event.getText().add(timeRange);
            }
        } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
            int flags = 0;
            // add the selection
            if (mNumDays == 1) {
                // if day view we need only hour information
                flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
            } else {
                flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
                        | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
            }
            long startTime = getSelectedTimeInMillis();
            long endTime = startTime   MILLIS_PER_HOUR;
            if (DateFormat.is24HourFormat(mParentActivity)) {
                flags |= DateUtils.FORMAT_24HOUR;
            }
            String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
                    flags);
            event.getText().add(timeRange);

            // add the selected event data if such
            if (mSelectedEvent != null) {
                Event selectedEvent = mSelectedEvent;
                if (mTempEventBundle == null) {
                    mTempEventBundle = new Bundle();
                }
                Bundle bundle = mTempEventBundle;
                bundle.clear();
                bundle.putLong("id", selectedEvent.id);
                bundle.putInt("color", selectedEvent.color);
                bundle.putCharSequence("title", selectedEvent.title);
                bundle.putCharSequence("location", selectedEvent.location);
                bundle.putBoolean("allDay", selectedEvent.allDay);
                bundle.putInt("startDay", selectedEvent.startDay);
                bundle.putInt("endDay", selectedEvent.endDay);
                bundle.putInt("startTime", selectedEvent.startTime);
                bundle.putInt("endTime", selectedEvent.endTime);
                bundle.putLong("startMillis", selectedEvent.startMillis);
                bundle.putLong("endMillis", selectedEvent.endMillis);
                bundle.putString("organizer", selectedEvent.organizer);
                bundle.putBoolean("guestsCanModify", selectedEvent.guestsCanModify);
                event.setParcelableData(bundle);
            }
        }

        // add day event count, events for same hour count and
        // the index of the selected event for the same hour
        int todayEventCount = 0;
        int sameHourEventCount = 0;
        int currentSameHourEventIndex = 0;
        int selectionHourStart = mSelectionHour * MINUTES_PER_HOUR;
        int selectionHourEnd = selectionHourStart   MINUTES_PER_HOUR;
        for (int i = 0, count = mEvents.size(); i < count; i  ) {
            Event calendarEvent = mEvents.get(i);
            if (calendarEvent.endDay == mSelectionDay) {
                todayEventCount  ;
                if (selectionHourStart >= calendarEvent.endTime
                        || selectionHourEnd <= calendarEvent.startTime) {
                    continue;
                }
                if (calendarEvent == mSelectedEvent) {
                    currentSameHourEventIndex = sameHourEventCount;
                }
                sameHourEventCount  ;
            }
        }
        event.setAddedCount(todayEventCount);
        event.setItemCount(sameHourEventCount);
        event.setCurrentItemIndex(currentSameHourEventIndex);

        return true;
    }

    private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
        float xCenter = x   mCellWidth / 2.0f;

        if (Utils.isSaturday(day, mStartDay)) {
            p.setColor(mWeek_saturdayColor);
        } else if (Utils.isSunday(day, mStartDay)) {
            p.setColor(mWeek_sundayColor);
        } else {
            p.setColor(mCalendarDateBannerTextColor);
        }

        int dateNum = mFirstDate   day;
        if (dateNum > mMonthLength) {
            dateNum -= mMonthLength;
        }

        String dateNumStr;
        // Add a leading zero if the date is a single digit
        if (dateNum < 10) {
            dateNumStr = "0"   dateNum;
        } else {
            dateNumStr = String.valueOf(dateNum);
        }

        DayHeader header = dayHeaders[day];
        if (header == null || header.cell != cell) {
            // The day header string is regenerated on every draw during drag and fling animation.
            // Caching day header since formatting the string takes surprising long time.

            dayHeaders[day] = new DayHeader();
            dayHeaders[day].cell = cell;
            dayHeaders[day].dateString = getResources().getString(
                    R.string.weekday_day, dateStr, dateNumStr);
        }
        dateStr = dayHeaders[day].dateString;

        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(mCalendarGridAreaBackground);
        r.top = 0;
        r.bottom = mBitmapHeight;
        r.left = 0;
        r.right = mViewWidth;
        canvas.drawRect(r, p);

        // Draw the horizontal grid lines
        p.setColor(mCalendarGridLineHorizontalColor);
        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(mCalendarGridLineVerticalColor);
        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 eventTextPaint = mEventTextPaint;

        // 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(mCalendarAllDayBackground);
        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(mCalendarGridAreaBackground);
        canvas.drawRect(r, p);

        // Draw the vertical grid lines
        p.setColor(mCalendarGridLineVerticalColor);
        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, eventTextPaint);
            drawEventText(event, rf, canvas, eventTextPaint, 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, eventTextPaint);
                drawEventText(event, rf, canvas, eventTextPaint, 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, Paint eventTextPaint) {
        // 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);
            eventTextPaint.setColor(mSelectedEventTextColor);
        } else {
            // Use the normal color for all-day events
            p.setColor(event.color);
            eventTextPaint.setColor(mEventTextColor);
        }

        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);

        rf.left  = 2;
        rf.right -= 2;
        return rf;
    }

    private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
        Paint eventTextPaint = mEventTextPaint;
        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, eventTextPaint);
            drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
        }

        if (date == mSelectionDay && !mSelectionAllDay && isFocused()
                && mSelectionMode != SELECTION_HIDDEN) {
            computeNeighbors();
            if (mSelectedEvent != null) {
                RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
                drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, 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;
        int prevBottom;
        int prevLeft;
        int prevRight;
        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(mParentActivity)) {
                    flags |= DateUtils.FORMAT_24HOUR;
                }
                String timeRange = Utils.formatDateRange(mParentActivity,
                        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, Paint eventTextPaint) {

        int color = event.color;

        // Fade visible boxes if event was declined.
        boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
        if (declined) {
            int alpha = color & 0xff000000;
            color &= 0x00ffffff;
            int red = (color & 0x00ff0000) >> 16;
            int green = (color & 0x0000ff00) >> 8;
            int blue = (color & 0x0000ff);
            color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
            color  = 0x7F7F7F   alpha;
        }

        // If this event is selected, then use the selection color
        if (mSelectedEvent == event) {
            if (mSelectionMode == SELECTION_PRESSED || mSelectionMode == SELECTION_SELECTED) {
                // Also, remember the last selected event that we drew
                mPrevSelectedEvent = event;
                p.setColor(mSelectionColor);
                eventTextPaint.setColor(mSelectedEventTextColor);
            } else if (mSelectionMode == SELECTION_LONGPRESS) {
                p.setColor(mSelectionColor);
                eventTextPaint.setColor(mSelectedEventTextColor);
            } else {
                p.setColor(color);
                eventTextPaint.setColor(mEventTextColor);
            }
        } else {
            p.setColor(color);
            eventTextPaint.setColor(mEventTextColor);
        }


        RectF rf = mRectF;
        rf.top = event.top;
        rf.bottom = event.bottom;
        rf.left = event.left;
        rf.right = event.right - 1;

        canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);

        // Draw a darker border
        float[] hsv = new float[3];
        Color.colorToHSV(p.getColor(), hsv);
        hsv[1] = 1.0f;
        hsv[2] *= 0.75f;
        mPaintBorder.setColor(Color.HSVToColor(hsv));
        canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);

        rf.left  = 2;
        rf.right -= 2;

        return rf;
    }

    private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");

    // Sanitize a string before passing it to drawText or else we get little
    // squares. For newlines and tabs before a comma, delete the character.
    // Otherwise, just replace them with a space.
    private String drawTextSanitizer(String string) {
        Matcher m = drawTextSanitizerFilter.matcher(string);
        string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
        return string;
    }

    private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
        if (!mDrawTextInEventRect) {
            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();

        text = drawTextSanitizer(text);

        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;
        }
        if (mLastPopupEventID == mSelectedEvent.id) {
            return;
        }

        mLastPopupEventID = mSelectedEvent.id;

        // 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(mParentActivity)) {
            flags |= DateUtils.FORMAT_24HOUR;
        }
        String timeRange = Utils.formatDateRange(mParentActivity,
                event.startMillis, event.endMillis, flags);
        TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
        timeView.setText(timeRange);

        TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
        final boolean empty = TextUtils.isEmpty(event.location);
        whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
        if (!empty) whereView.setText(event.location);

        mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
        postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    }

    // 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;
        getHandler().removeCallbacks(mContinueScroll);
    }

    void doSingleTapUp(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        int selectedDay = mSelectionDay;
        int selectedHour = mSelectionHour;

        boolean validPosition = setSelectionFromPosition(x, y);
        if (!validPosition) {
            // return if the touch wasn't on an area of concern
            return;
        }

        mSelectionMode = SELECTION_SELECTED;
        mRedrawScreen = true;
        invalidate();

        boolean launchNewView = false;
        if (mSelectedEvent != null) {
            // If the tap is on an event, launch the "View event" view
            launchNewView = true;
        } else if (mSelectedEvent == null && selectedDay == mSelectionDay
                && selectedHour == mSelectionHour) {
            // If the tap is on an already selected hour slot,
            // then launch the Day/Agenda view. Otherwise, just select the hour
            // slot.
            launchNewView = true;
        }

        if (launchNewView) {
            switchViews(false /* not the trackball */);
        }
    }

    void doLongPress(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        boolean validPosition = setSelectionFromPosition(x, y);
        if (!validPosition) {
            // return if the touch wasn't on an area of concern
            return;
        }

        mSelectionMode = SELECTION_LONGPRESS;
        mRedrawScreen = true;
        invalidate();
        performLongClick();
    }

    void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
        // 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);
        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.layout(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);
                    mViewStartX = 0;
                    return true;
                } 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();
        }

        final long startMillis = getSelectedTimeInMillis();
        int flags = DateUtils.FORMAT_SHOW_TIME
                | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
                | DateUtils.FORMAT_SHOW_WEEKDAY;
        final String title = Utils.formatDateRange(mParentActivity, startMillis, startMillis,
                flags);
        menu.setHeaderTitle(title);

        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);

                int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
                if (accessLevel == ACCESS_LEVEL_EDIT) {
                    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');
                }

                if (accessLevel >= ACCESS_LEVEL_DELETE) {
                    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);

                int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
                if (accessLevel == ACCESS_LEVEL_EDIT) {
                    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');
                }

                if (accessLevel >= ACCESS_LEVEL_DELETE) {
                    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.show_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.show_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.show_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.show_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(mParentActivity, 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(mParentActivity, 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();
                    Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
                    break;
                }
                case MenuHelper.MENU_AGENDA: {
                    long startMillis = getSelectedTimeInMillis();
                    Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
                    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(mParentActivity, 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 int getEventAccessLevel(Context context, Event e) {
        ContentResolver cr = context.getContentResolver();

        int visibility = Calendars.NO_ACCESS;

        // 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) {
            return ACCESS_LEVEL_NONE;
        }

        if (cursor.getCount() == 0) {
            cursor.close();
            return ACCESS_LEVEL_NONE;
        }

        cursor.moveToFirst();
        long calId = cursor.getLong(0);
        cursor.close();

        Uri uri = Calendars.CONTENT_URI;
        String[] whereArgs = new String[] { String.valueOf(calId) };
        cursor = cr.query(uri, CALENDARS_PROJECTION, CALENDARS_WHERE, whereArgs, null);

        String calendarOwnerAccount = null;
        if (cursor != null) {
            cursor.moveToFirst();
            visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
            calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
            cursor.close();
        }

        if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
            return ACCESS_LEVEL_NONE;
        }

        if (e.guestsCanModify) {
            return ACCESS_LEVEL_EDIT;
        }

        if (!TextUtils.isEmpty(calendarOwnerAccount) &&
                calendarOwnerAccount.equalsIgnoreCase(e.organizer)) {
            return ACCESS_LEVEL_EDIT;
        }

        return ACCESS_LEVEL_DELETE;
    }

    /**
     * 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 = Utils.formatDateRange(mParentActivity,
//                        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 and timers.
     */
    public void cleanup() {
        // Protect against null-pointer exceptions
        if (mPopup != null) {
            mPopup.dismiss();
        }
        mLastPopupEventID = INVALID_EVENT_ID;
        Handler handler = getHandler();
        if (handler != null) {
            handler.removeCallbacks(mDismissPopup);
            handler.removeCallbacks(mUpdateCurrentTime);
        }

        // Turn off redraw
        mRemeasure = false;
        mRedrawScreen = false;

        // clear the cached values for accessibility support
        mPrevSelectionDay = 0;
        mPrevSelectionHour = 0;
        mPrevTitleTextViewText = null;
    }

    /**
     * Restart the update timer
     */
    public void updateView() {
        mUpdateTZ.run();
        post(mUpdateCurrentTime);
    }

    @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();
            }
        }
    }

    class UpdateCurrentTime implements Runnable {
        public void run() {
            long currentTime = System.currentTimeMillis();
            mCurrentTime.set(currentTime);
            //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
            postDelayed(mUpdateCurrentTime,
                    UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
            mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
            mRedrawScreen = true;
            invalidate();
        }
    }
}


标签: 日历 安卓

实例下载地址

安卓日历源码下载

不能下载?内容有错? 点击这里报错 + 投诉 + 提问

好例子网口号:伸出你的我的手 — 分享

网友评论

发表评论

(您的评论需要经过审核才能显示)

查看所有0条评论>>

小贴士

感谢您为本站写下的评论,您的评论对其它用户来说具有重要的参考价值,所以请认真填写。

  • 类似“顶”、“沙发”之类没有营养的文字,对勤劳贡献的楼主来说是令人沮丧的反馈信息。
  • 相信您也不想看到一排文字/表情墙,所以请不要反馈意义不大的重复字符,也请尽量不要纯表情的回复。
  • 提问之前请再仔细看一遍楼主的说明,或许是您遗漏了。
  • 请勿到处挖坑绊人、招贴广告。既占空间让人厌烦,又没人会搭理,于人于己都无利。

关于好例子网

本站旨在为广大IT学习爱好者提供一个非营利性互相学习交流分享平台。本站所有资源都可以被免费获取学习研究。本站资源来自网友分享,对搜索内容的合法性不具有预见性、识别性、控制性,仅供学习研究,请务必在下载后24小时内给予删除,不得用于其他任何用途,否则后果自负。基于互联网的特殊性,平台无法对用户传输的作品、信息、内容的权属或合法性、安全性、合规性、真实性、科学性、完整权、有效性等进行实质审查;无论平台是否已进行审查,用户均应自行承担因其传输的作品、信息、内容而可能或已经产生的侵权或权属纠纷等法律责任。本站所有资源不代表本站的观点或立场,基于网友分享,根据中国法律《信息网络传播权保护条例》第二十二与二十三条之规定,若资源存在侵权或相关问题请联系本站客服人员,点此联系我们。关于更多版权及免责申明参见 版权及免责申明

;
报警