欢迎来到传世资源网!
加载中...
正在加载,请耐心等待...
本站为收藏、学习站,如有侵权,请联系管理员删除!

安卓日历源码下载_

介绍 评论 失效链接反馈

android 日历

安卓日历源码下载_ Android平台开发-第1张安卓日历源码下载_ Android平台开发-第2张

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

下载声明:

本站资源均有第三方用户自行上传分享推荐,非本站自制,仅供玩家做交流学习之用!切勿用于商业用途!游戏作品版权归原作者享有,如有版权问题,请附带版权证明至邮件,本平台将应您的要求删除。
相关推荐:

评论

发表评论必须先登陆, 您可以 登陆 或者 注册新账号 !


在线咨询: 问题反馈
客服QQ:174666394

有问题请留言,看到后及时答复