Home | History | Annotate | Download | only in calendar
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.calendar;
     18 
     19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
     20 import static android.provider.Calendar.EVENT_END_TIME;
     21 
     22 import android.content.ContentResolver;
     23 import android.content.ContentUris;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.res.Resources;
     27 import android.content.res.TypedArray;
     28 import android.database.Cursor;
     29 import android.graphics.Bitmap;
     30 import android.graphics.Canvas;
     31 import android.graphics.Color;
     32 import android.graphics.Paint;
     33 import android.graphics.Paint.Style;
     34 import android.graphics.Path;
     35 import android.graphics.Path.Direction;
     36 import android.graphics.PorterDuff;
     37 import android.graphics.Rect;
     38 import android.graphics.RectF;
     39 import android.graphics.Typeface;
     40 import android.net.Uri;
     41 import android.os.Bundle;
     42 import android.os.Handler;
     43 import android.provider.Calendar.Attendees;
     44 import android.provider.Calendar.Calendars;
     45 import android.provider.Calendar.Events;
     46 import android.text.TextUtils;
     47 import android.text.format.DateFormat;
     48 import android.text.format.DateUtils;
     49 import android.text.format.Time;
     50 import android.util.Log;
     51 import android.view.ContextMenu;
     52 import android.view.ContextMenu.ContextMenuInfo;
     53 import android.view.Gravity;
     54 import android.view.KeyEvent;
     55 import android.view.LayoutInflater;
     56 import android.view.MenuItem;
     57 import android.view.MotionEvent;
     58 import android.view.View;
     59 import android.view.ViewConfiguration;
     60 import android.view.ViewGroup;
     61 import android.view.WindowManager;
     62 import android.view.accessibility.AccessibilityEvent;
     63 import android.view.accessibility.AccessibilityManager;
     64 import android.widget.ImageView;
     65 import android.widget.PopupWindow;
     66 import android.widget.TextView;
     67 
     68 import java.util.ArrayList;
     69 import java.util.Calendar;
     70 import java.util.Locale;
     71 import java.util.TimeZone;
     72 import java.util.regex.Matcher;
     73 import java.util.regex.Pattern;
     74 
     75 /**
     76  * This is the base class for a set of classes that implement views (day view
     77  * and week view to start with) that share some common code.
     78   */
     79 public class CalendarView extends View
     80         implements View.OnCreateContextMenuListener, View.OnClickListener {
     81 
     82     private static float mScale = 0; // Used for supporting different screen densities
     83     private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
     84 
     85     private boolean mOnFlingCalled;
     86     /**
     87      * ID of the last event which was displayed with the toast popup.
     88      *
     89      * This is used to prevent popping up multiple quick views for the same event, especially
     90      * during calendar syncs. This becomes valid when an event is selected, either by default
     91      * on starting calendar or by scrolling to an event. It becomes invalid when the user
     92      * explicitly scrolls to an empty time slot, changes views, or deletes the event.
     93      */
     94     private long mLastPopupEventID;
     95 
     96     protected CalendarApplication mCalendarApp;
     97     protected CalendarActivity mParentActivity;
     98 
     99     // This runs when we need to update the tz
    100     private Runnable mUpdateTZ = new Runnable() {
    101         @Override
    102         public void run() {
    103             String tz = Utils.getTimeZone(mContext, this);
    104             // BaseDate we want to keep on the same day, so we swap tz
    105             mBaseDate.timezone = tz;
    106             mBaseDate.normalize(true);
    107             // CurrentTime we want to keep at the same absolute time, so we
    108             // call switch tz
    109             mCurrentTime.switchTimezone(tz);
    110             mTimeZone = TimeZone.getTimeZone(tz);
    111             recalc();
    112             mTitleTextView.setText(mDateRange);
    113         }
    114     };
    115     private Context mContext;
    116 
    117     private static final String[] CALENDARS_PROJECTION = new String[] {
    118         Calendars._ID,          // 0
    119         Calendars.ACCESS_LEVEL, // 1
    120         Calendars.OWNER_ACCOUNT, // 2
    121     };
    122     private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
    123     private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
    124     private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
    125 
    126     private static final String[] ATTENDEES_PROJECTION = new String[] {
    127         Attendees._ID,                      // 0
    128         Attendees.ATTENDEE_RELATIONSHIP,    // 1
    129     };
    130     private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
    131     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
    132 
    133     private static float SMALL_ROUND_RADIUS = 3.0F;
    134 
    135     private static final int FROM_NONE = 0;
    136     private static final int FROM_ABOVE = 1;
    137     private static final int FROM_BELOW = 2;
    138     private static final int FROM_LEFT = 4;
    139     private static final int FROM_RIGHT = 8;
    140 
    141     private static final int ACCESS_LEVEL_NONE = 0;
    142     private static final int ACCESS_LEVEL_DELETE = 1;
    143     private static final int ACCESS_LEVEL_EDIT = 2;
    144 
    145     private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
    146 
    147     private ContinueScroll mContinueScroll = new ContinueScroll();
    148 
    149     static private class DayHeader{
    150         int cell;
    151         String dateString;
    152     }
    153 
    154     private DayHeader[] dayHeaders = new DayHeader[32];
    155 
    156     // Make this visible within the package for more informative debugging
    157     Time mBaseDate;
    158     private Time mCurrentTime;
    159     //Update the current time line every five minutes if the window is left open that long
    160     private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
    161     private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
    162     private int mTodayJulianDay;
    163 
    164     private Typeface mBold = Typeface.DEFAULT_BOLD;
    165     private int mFirstJulianDay;
    166     private int mLastJulianDay;
    167 
    168     private int mMonthLength;
    169     private int mFirstDate;
    170     private int[] mEarliestStartHour;    // indexed by the week day offset
    171     private boolean[] mHasAllDayEvent;   // indexed by the week day offset
    172 
    173     private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
    174 
    175     /**
    176      * This variable helps to avoid unnecessarily reloading events by keeping
    177      * track of the start millis parameter used for the most recent loading
    178      * of events.  If the next reload matches this, then the events are not
    179      * reloaded.  To force a reload, set this to zero (this is set to zero
    180      * in the method clearCachedEvents()).
    181      */
    182     private long mLastReloadMillis;
    183 
    184     private ArrayList<Event> mEvents = new ArrayList<Event>();
    185     private int mSelectionDay;        // Julian day
    186     private int mSelectionHour;
    187 
    188     /* package private so that CalendarActivity can read it when creating new
    189      * events
    190      */
    191     boolean mSelectionAllDay;
    192 
    193     private int mCellWidth;
    194 
    195     // Pre-allocate these objects and re-use them
    196     private Rect mRect = new Rect();
    197     private RectF mRectF = new RectF();
    198     private Rect mSrcRect = new Rect();
    199     private Rect mDestRect = new Rect();
    200     private Paint mPaint = new Paint();
    201     private Paint mPaintBorder = new Paint();
    202     private Paint mEventTextPaint = new Paint();
    203     private Paint mSelectionPaint = new Paint();
    204     private Path mPath = new Path();
    205 
    206     protected boolean mDrawTextInEventRect;
    207     private int mStartDay;
    208 
    209     private PopupWindow mPopup;
    210     private View mPopupView;
    211 
    212     // The number of milliseconds to show the popup window
    213     private static final int POPUP_DISMISS_DELAY = 3000;
    214     private DismissPopup mDismissPopup = new DismissPopup();
    215 
    216     // For drawing to an off-screen Canvas
    217     private Bitmap mBitmap;
    218     private Canvas mCanvas;
    219     private boolean mRedrawScreen = true;
    220     private boolean mRemeasure = true;
    221 
    222     private final EventLoader mEventLoader;
    223     protected final EventGeometry mEventGeometry;
    224 
    225     private static final int DAY_GAP = 1;
    226     private static final int HOUR_GAP = 1;
    227     private static int SINGLE_ALLDAY_HEIGHT = 20;
    228     private static int MAX_ALLDAY_HEIGHT = 72;
    229     private static int ALLDAY_TOP_MARGIN = 3;
    230     private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
    231 
    232     /* The extra space to leave above the text in all-day events */
    233     private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
    234 
    235     /* The extra space to leave above the text in normal events */
    236     private static final int NORMAL_TEXT_TOP_MARGIN = 2;
    237 
    238     private static final int HOURS_LEFT_MARGIN = 2;
    239     private static final int HOURS_RIGHT_MARGIN = 4;
    240     private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
    241 
    242     private static int CURRENT_TIME_LINE_HEIGHT = 2;
    243     private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
    244     private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6;
    245     private static int CURRENT_TIME_MARKER_HEIGHT = 6;
    246     private static int CURRENT_TIME_MARKER_WIDTH = 8;
    247     private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1;
    248 
    249     /* package */ static final int MINUTES_PER_HOUR = 60;
    250     /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
    251     /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
    252     /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
    253     /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
    254 
    255     private static int NORMAL_FONT_SIZE = 12;
    256     private static int EVENT_TEXT_FONT_SIZE = 12;
    257     private static int HOURS_FONT_SIZE = 12;
    258     private static int AMPM_FONT_SIZE = 9;
    259     private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
    260     private static final int MAX_EVENT_TEXT_LEN = 500;
    261     private static float MIN_EVENT_HEIGHT = 15.0F;  // in pixels
    262 
    263     // This value forces the position calculator to take care of the overwap which can't be
    264     // detected from the view of event time but actually is detected when rendering them.
    265     //
    266     // Detail:
    267     // Imagine there are two events: A (from 1:00pm to 1:01pm) and B (from 1:02pm to 2:00pm).
    268     // The position calculator (Event#doComputePositions()), marks them as "not overwrapped"
    269     // as A finishes before B's begin time, so those events are put on the same column
    270     // (or, horizontal position).
    271     // From the view of renderer, however, the actual rectangle for A is larger than "1 min."
    272     // for accomodating at least 1 line of text in it.
    273     // As a result, A's rectangle is overwrapped by B's, and A becomes hard to be touched
    274     // without trackball or DPAD (as, it is beneath B from the user' view).
    275     // This values forces the original calculator to take care of the actual overwrap detected in
    276     // rendering time.
    277     //
    278     // Note:
    279     // Theoretically we can calcurate an ideal value for this purpose by making the calculator
    280     // understand the relation between each event and pixel-level height of actual rectangles,
    281     // but we don't do so as currently the calculator doesn't have convenient way to obtain
    282     // necessary values for the calculation.
    283     /* package */ static long EVENT_OVERWRAP_MARGIN_TIME = MILLIS_PER_MINUTE * 15;
    284 
    285     private static int mSelectionColor;
    286     private static int mPressedColor;
    287     private static int mSelectedEventTextColor;
    288     private static int mEventTextColor;
    289     private static int mWeek_saturdayColor;
    290     private static int mWeek_sundayColor;
    291     private static int mCalendarDateBannerTextColor;
    292     private static int mCalendarAllDayBackground;
    293     private static int mCalendarAmPmLabel;
    294     private static int mCalendarDateBannerBackground;
    295     private static int mCalendarDateSelected;
    296     private static int mCalendarGridAreaBackground;
    297     private static int mCalendarGridAreaSelected;
    298     private static int mCalendarGridLineHorizontalColor;
    299     private static int mCalendarGridLineVerticalColor;
    300     private static int mCalendarHourBackground;
    301     private static int mCalendarHourLabel;
    302     private static int mCalendarHourSelected;
    303     private static int mCurrentTimeMarkerColor;
    304     private static int mCurrentTimeLineColor;
    305     private static int mCurrentTimeMarkerBorderColor;
    306 
    307     private int mViewStartX;
    308     private int mViewStartY;
    309     private int mMaxViewStartY;
    310     private int mBitmapHeight;
    311     private int mViewHeight;
    312     private int mViewWidth;
    313     private int mGridAreaHeight;
    314     private int mCellHeight;
    315     private int mScrollStartY;
    316     private int mPreviousDirection;
    317     private int mPreviousDistanceX;
    318 
    319     private int mHoursTextHeight;
    320     private int mEventTextAscent;
    321     private int mEventTextHeight;
    322     private int mAllDayHeight;
    323     private int mBannerPlusMargin;
    324     private int mMaxAllDayEvents;
    325 
    326     protected int mNumDays = 7;
    327     private int mNumHours = 10;
    328     private int mHoursWidth;
    329     private int mDateStrWidth;
    330     private int mFirstCell;
    331     private int mFirstHour = -1;
    332     private int mFirstHourOffset;
    333     private String[] mHourStrs;
    334     private String[] mDayStrs;
    335     private String[] mDayStrs2Letter;
    336     private boolean mIs24HourFormat;
    337 
    338     private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
    339     private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
    340     private boolean mComputeSelectedEvents;
    341     private Event mSelectedEvent;
    342     private Event mPrevSelectedEvent;
    343     private Rect mPrevBox = new Rect();
    344     protected final Resources mResources;
    345     private String mAmString;
    346     private String mPmString;
    347     private DeleteEventHelper mDeleteEventHelper;
    348 
    349     private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
    350 
    351     /**
    352      * The initial state of the touch mode when we enter this view.
    353      */
    354     private static final int TOUCH_MODE_INITIAL_STATE = 0;
    355 
    356     /**
    357      * Indicates we just received the touch event and we are waiting to see if
    358      * it is a tap or a scroll gesture.
    359      */
    360     private static final int TOUCH_MODE_DOWN = 1;
    361 
    362     /**
    363      * Indicates the touch gesture is a vertical scroll
    364      */
    365     private static final int TOUCH_MODE_VSCROLL = 0x20;
    366 
    367     /**
    368      * Indicates the touch gesture is a horizontal scroll
    369      */
    370     private static final int TOUCH_MODE_HSCROLL = 0x40;
    371 
    372     private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
    373 
    374     /**
    375      * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
    376      */
    377     private static final int SELECTION_HIDDEN = 0;
    378     private static final int SELECTION_PRESSED = 1;
    379     private static final int SELECTION_SELECTED = 2;
    380     private static final int SELECTION_LONGPRESS = 3;
    381 
    382     private int mSelectionMode = SELECTION_HIDDEN;
    383 
    384     private boolean mScrolling = false;
    385 
    386     private TimeZone mTimeZone;
    387     private String mDateRange;
    388     private TextView mTitleTextView;
    389 
    390     // Accessibility support related members
    391 
    392     private int mPrevSelectionDay;
    393     private int mPrevSelectionHour;
    394     private CharSequence mPrevTitleTextViewText;
    395     private Bundle mTempEventBundle;
    396 
    397     public CalendarView(CalendarActivity activity) {
    398         super(activity);
    399         if (mScale == 0) {
    400             mScale = getContext().getResources().getDisplayMetrics().density;
    401             if (mScale != 1) {
    402                 SINGLE_ALLDAY_HEIGHT *= mScale;
    403                 MAX_ALLDAY_HEIGHT *= mScale;
    404                 ALLDAY_TOP_MARGIN *= mScale;
    405                 MAX_ALLDAY_EVENT_HEIGHT *= mScale;
    406 
    407                 NORMAL_FONT_SIZE *= mScale;
    408                 EVENT_TEXT_FONT_SIZE *= mScale;
    409                 HOURS_FONT_SIZE *= mScale;
    410                 AMPM_FONT_SIZE *= mScale;
    411                 MIN_CELL_WIDTH_FOR_TEXT *= mScale;
    412                 MIN_EVENT_HEIGHT *= mScale;
    413 
    414                 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
    415 
    416                 CURRENT_TIME_MARKER_HEIGHT *= mScale;
    417                 CURRENT_TIME_MARKER_WIDTH *= mScale;
    418                 CURRENT_TIME_LINE_HEIGHT *= mScale;
    419                 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
    420                 CURRENT_TIME_MARKER_INNER_WIDTH *= mScale;
    421                 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;
    422 
    423                 SMALL_ROUND_RADIUS *= mScale;
    424             }
    425         }
    426 
    427         mResources = activity.getResources();
    428         mEventLoader = activity.mEventLoader;
    429         mEventGeometry = new EventGeometry();
    430         mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
    431         mEventGeometry.setHourGap(HOUR_GAP);
    432         mParentActivity = activity;
    433         mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
    434         mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
    435         mLastPopupEventID = INVALID_EVENT_ID;
    436 
    437         init(activity);
    438     }
    439 
    440     private void init(Context context) {
    441         setFocusable(true);
    442 
    443         // Allow focus in touch mode so that we can do keyboard shortcuts
    444         // even after we've entered touch mode.
    445         setFocusableInTouchMode(true);
    446         setClickable(true);
    447         setOnCreateContextMenuListener(this);
    448 
    449         mStartDay = Utils.getFirstDayOfWeek();
    450 
    451         mTimeZone = TimeZone.getTimeZone(Utils.getTimeZone(context, mUpdateTZ));
    452 
    453         mContext = context;
    454         mCurrentTime = new Time(Utils.getTimeZone(context, mUpdateTZ));
    455         long currentTime = System.currentTimeMillis();
    456         mCurrentTime.set(currentTime);
    457         //The % makes it go off at the next increment of 5 minutes.
    458         postDelayed(mUpdateCurrentTime,
    459                 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
    460         mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
    461 
    462         mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
    463         mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
    464         mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
    465         mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
    466         mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
    467         mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
    468         mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
    469         mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
    470         mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
    471         mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
    472         mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
    473         mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
    474         mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
    475         mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
    476         mSelectionColor = mResources.getColor(R.color.selection);
    477         mPressedColor = mResources.getColor(R.color.pressed);
    478         mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
    479         mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
    480         mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker);
    481         mCurrentTimeLineColor = mResources.getColor(R.color.current_time_line);
    482         mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border);
    483         mEventTextPaint.setColor(mEventTextColor);
    484         mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
    485         mEventTextPaint.setTextAlign(Paint.Align.LEFT);
    486         mEventTextPaint.setAntiAlias(true);
    487 
    488         int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
    489         Paint p = mSelectionPaint;
    490         p.setColor(gridLineColor);
    491         p.setStyle(Style.STROKE);
    492         p.setStrokeWidth(2.0f);
    493         p.setAntiAlias(false);
    494 
    495         p = mPaint;
    496         p.setAntiAlias(true);
    497 
    498         mPaintBorder.setColor(0xffc8c8c8);
    499         mPaintBorder.setStyle(Style.STROKE);
    500         mPaintBorder.setAntiAlias(true);
    501         mPaintBorder.setStrokeWidth(2.0f);
    502 
    503         // Allocate space for 2 weeks worth of weekday names so that we can
    504         // easily start the week display at any week day.
    505         mDayStrs = new String[14];
    506 
    507         // Also create an array of 2-letter abbreviations.
    508         mDayStrs2Letter = new String[14];
    509 
    510         for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
    511             int index = i - Calendar.SUNDAY;
    512             // e.g. Tue for Tuesday
    513             mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
    514             mDayStrs[index + 7] = mDayStrs[index];
    515             // e.g. Tu for Tuesday
    516             mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
    517 
    518             // If we don't have 2-letter day strings, fall back to 1-letter.
    519             if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
    520                 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
    521             }
    522 
    523             mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
    524         }
    525 
    526         // Figure out how much space we need for the 3-letter abbrev names
    527         // in the worst case.
    528         p.setTextSize(NORMAL_FONT_SIZE);
    529         p.setTypeface(mBold);
    530         String[] dateStrs = {" 28", " 30"};
    531         mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
    532         mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
    533 
    534         p.setTextSize(HOURS_FONT_SIZE);
    535         p.setTypeface(null);
    536         updateIs24HourFormat();
    537 
    538         mAmString = DateUtils.getAMPMString(Calendar.AM);
    539         mPmString = DateUtils.getAMPMString(Calendar.PM);
    540         String[] ampm = {mAmString, mPmString};
    541         p.setTextSize(AMPM_FONT_SIZE);
    542         mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
    543         mHoursWidth += HOURS_MARGIN;
    544 
    545         LayoutInflater inflater;
    546         inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    547         mPopupView = inflater.inflate(R.layout.bubble_event, null);
    548         mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
    549                 ViewGroup.LayoutParams.MATCH_PARENT,
    550                 ViewGroup.LayoutParams.WRAP_CONTENT));
    551         mPopup = new PopupWindow(context);
    552         mPopup.setContentView(mPopupView);
    553         Resources.Theme dialogTheme = getResources().newTheme();
    554         dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
    555         TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
    556             android.R.attr.windowBackground });
    557         mPopup.setBackgroundDrawable(ta.getDrawable(0));
    558         ta.recycle();
    559 
    560         // Enable touching the popup window
    561         mPopupView.setOnClickListener(this);
    562 
    563         mBaseDate = new Time(Utils.getTimeZone(context, mUpdateTZ));
    564         long millis = System.currentTimeMillis();
    565         mBaseDate.set(millis);
    566 
    567         mEarliestStartHour = new int[mNumDays];
    568         mHasAllDayEvent = new boolean[mNumDays];
    569 
    570         mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
    571         mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
    572     }
    573 
    574     /**
    575      * This is called when the popup window is pressed.
    576      */
    577     public void onClick(View v) {
    578         if (v == mPopupView) {
    579             // Pretend it was a trackball click because that will always
    580             // jump to the "View event" screen.
    581             switchViews(true /* trackball */);
    582         }
    583     }
    584 
    585     public void updateIs24HourFormat() {
    586         mIs24HourFormat = DateFormat.is24HourFormat(mParentActivity);
    587         mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
    588     }
    589 
    590     /**
    591      * Returns the start of the selected time in milliseconds since the epoch.
    592      *
    593      * @return selected time in UTC milliseconds since the epoch.
    594      */
    595     long getSelectedTimeInMillis() {
    596         Time time = new Time(mBaseDate);
    597         time.setJulianDay(mSelectionDay);
    598         time.hour = mSelectionHour;
    599 
    600         // We ignore the "isDst" field because we want normalize() to figure
    601         // out the correct DST value and not adjust the selected time based
    602         // on the current setting of DST.
    603         return time.normalize(true /* ignore isDst */);
    604     }
    605 
    606     Time getSelectedTime() {
    607         Time time = new Time(mBaseDate);
    608         time.setJulianDay(mSelectionDay);
    609         time.hour = mSelectionHour;
    610 
    611         // We ignore the "isDst" field because we want normalize() to figure
    612         // out the correct DST value and not adjust the selected time based
    613         // on the current setting of DST.
    614         time.normalize(true /* ignore isDst */);
    615         return time;
    616     }
    617 
    618     /**
    619      * Returns the start of the selected time in minutes since midnight,
    620      * local time.  The derived class must ensure that this is consistent
    621      * with the return value from getSelectedTimeInMillis().
    622      */
    623     int getSelectedMinutesSinceMidnight() {
    624         return mSelectionHour * MINUTES_PER_HOUR;
    625     }
    626 
    627     public void setSelectedDay(Time time) {
    628         mBaseDate.set(time);
    629         mSelectionHour = mBaseDate.hour;
    630         mSelectedEvent = null;
    631         mPrevSelectedEvent = null;
    632         long millis = mBaseDate.toMillis(false /* use isDst */);
    633         mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
    634         mSelectedEvents.clear();
    635         mComputeSelectedEvents = true;
    636 
    637         // Force a recalculation of the first visible hour
    638         mFirstHour = -1;
    639         recalc();
    640         mTitleTextView.setText(mDateRange);
    641 
    642         // Force a redraw of the selection box.
    643         mSelectionMode = SELECTION_SELECTED;
    644         mRedrawScreen = true;
    645         mRemeasure = true;
    646         invalidate();
    647     }
    648 
    649     public Time getSelectedDay() {
    650         Time time = new Time(mBaseDate);
    651         time.setJulianDay(mSelectionDay);
    652         time.hour = mSelectionHour;
    653 
    654         // We ignore the "isDst" field because we want normalize() to figure
    655         // out the correct DST value and not adjust the selected time based
    656         // on the current setting of DST.
    657         time.normalize(true /* ignore isDst */);
    658         return time;
    659     }
    660 
    661     private void recalc() {
    662         // Set the base date to the beginning of the week if we are displaying
    663         // 7 days at a time.
    664         if (mNumDays == 7) {
    665             int dayOfWeek = mBaseDate.weekDay;
    666             int diff = dayOfWeek - mStartDay;
    667             if (diff != 0) {
    668                 if (diff < 0) {
    669                     diff += 7;
    670                 }
    671                 mBaseDate.monthDay -= diff;
    672                 mBaseDate.normalize(true /* ignore isDst */);
    673             }
    674         }
    675 
    676         long start = mBaseDate.normalize(true /* use isDst */);
    677         long end = start;
    678         mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
    679         mLastJulianDay = mFirstJulianDay + mNumDays - 1;
    680 
    681         mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
    682         mFirstDate = mBaseDate.monthDay;
    683 
    684         int flags = DateUtils.FORMAT_SHOW_YEAR;
    685         if (DateFormat.is24HourFormat(mParentActivity)) {
    686             flags |= DateUtils.FORMAT_24HOUR;
    687         }
    688         if (mNumDays > 1) {
    689             mBaseDate.monthDay += mNumDays - 1;
    690             end = mBaseDate.toMillis(true /* ignore isDst */);
    691             mBaseDate.monthDay -= mNumDays - 1;
    692             flags |= DateUtils.FORMAT_NO_MONTH_DAY;
    693         } else {
    694             flags |= DateUtils.FORMAT_SHOW_WEEKDAY
    695                     | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
    696         }
    697 
    698         mDateRange = Utils.formatDateRange(mParentActivity, start, end, flags);
    699 
    700         if (!TextUtils.equals(Utils.getTimeZone(mContext, mUpdateTZ), Time.getCurrentTimezone())) {
    701             flags = DateUtils.FORMAT_SHOW_TIME;
    702             if (DateFormat.is24HourFormat(mParentActivity)) {
    703                 flags |= DateUtils.FORMAT_24HOUR;
    704             }
    705             start = System.currentTimeMillis();
    706 
    707             String tz = Utils.getTimeZone(mContext, mUpdateTZ);
    708             boolean isDST = mBaseDate.isDst != 0;
    709             StringBuilder title = new StringBuilder(mDateRange);
    710             title.append(" (").append(Utils.formatDateRange(mContext, start, start, flags))
    711                     .append(" ")
    712                     .append(mTimeZone.getDisplayName(isDST, TimeZone.SHORT, Locale.getDefault()))
    713                     .append(")");
    714             mDateRange = title.toString();
    715         }
    716         // Do not set the title here because this is called when executing
    717         // initNextView() to prepare the Day view when sliding the finger
    718         // horizontally but we don't always want to change the title.  And
    719         // if we change the title here and then change it back in the caller
    720         // then we get an annoying flicker.
    721     }
    722 
    723     void setDetailedView(String detailedView) {
    724         mDetailedView = detailedView;
    725     }
    726 
    727     @Override
    728     protected void onSizeChanged(int width, int height, int oldw, int oldh) {
    729         mViewWidth = width;
    730         mViewHeight = height;
    731         int gridAreaWidth = width - mHoursWidth;
    732         mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
    733 
    734         Paint p = new Paint();
    735         p.setTextSize(NORMAL_FONT_SIZE);
    736         int bannerTextHeight = (int) Math.abs(p.ascent());
    737 
    738         p.setTextSize(HOURS_FONT_SIZE);
    739         mHoursTextHeight = (int) Math.abs(p.ascent());
    740 
    741         p.setTextSize(EVENT_TEXT_FONT_SIZE);
    742         float ascent = -p.ascent();
    743         mEventTextAscent = (int) Math.ceil(ascent);
    744         float totalHeight = ascent + p.descent();
    745         mEventTextHeight = (int) Math.ceil(totalHeight);
    746 
    747         if (mNumDays > 1) {
    748             mBannerPlusMargin = bannerTextHeight + 14;
    749         } else {
    750             mBannerPlusMargin = 0;
    751         }
    752 
    753         remeasure(width, height);
    754     }
    755 
    756     // Measures the space needed for various parts of the view after
    757     // loading new events.  This can change if there are all-day events.
    758     private void remeasure(int width, int height) {
    759 
    760         // First, clear the array of earliest start times, and the array
    761         // indicating presence of an all-day event.
    762         for (int day = 0; day < mNumDays; day++) {
    763             mEarliestStartHour[day] = 25;  // some big number
    764             mHasAllDayEvent[day] = false;
    765         }
    766 
    767         // Compute the space needed for the all-day events, if any.
    768         // Make a pass over all the events, and keep track of the maximum
    769         // number of all-day events in any one day.  Also, keep track of
    770         // the earliest event in each day.
    771         int maxAllDayEvents = 0;
    772         ArrayList<Event> events = mEvents;
    773         int len = events.size();
    774         for (int ii = 0; ii < len; ii++) {
    775             Event event = events.get(ii);
    776             if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
    777                 continue;
    778             if (event.allDay) {
    779                 int max = event.getColumn() + 1;
    780                 if (maxAllDayEvents < max) {
    781                     maxAllDayEvents = max;
    782                 }
    783                 int daynum = event.startDay - mFirstJulianDay;
    784                 int durationDays = event.endDay - event.startDay + 1;
    785                 if (daynum < 0) {
    786                     durationDays += daynum;
    787                     daynum = 0;
    788                 }
    789                 if (daynum + durationDays > mNumDays) {
    790                     durationDays = mNumDays - daynum;
    791                 }
    792                 for (int day = daynum; durationDays > 0; day++, durationDays--) {
    793                     mHasAllDayEvent[day] = true;
    794                 }
    795             } else {
    796                 int daynum = event.startDay - mFirstJulianDay;
    797                 int hour = event.startTime / 60;
    798                 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
    799                     mEarliestStartHour[daynum] = hour;
    800                 }
    801 
    802                 // Also check the end hour in case the event spans more than
    803                 // one day.
    804                 daynum = event.endDay - mFirstJulianDay;
    805                 hour = event.endTime / 60;
    806                 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
    807                     mEarliestStartHour[daynum] = hour;
    808                 }
    809             }
    810         }
    811         mMaxAllDayEvents = maxAllDayEvents;
    812 
    813         mFirstCell = mBannerPlusMargin;
    814         int allDayHeight = 0;
    815         if (maxAllDayEvents > 0) {
    816             // If there is at most one all-day event per day, then use less
    817             // space (but more than the space for a single event).
    818             if (maxAllDayEvents == 1) {
    819                 allDayHeight = SINGLE_ALLDAY_HEIGHT;
    820             } else {
    821                 // Allow the all-day area to grow in height depending on the
    822                 // number of all-day events we need to show, up to a limit.
    823                 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
    824                 if (allDayHeight > MAX_ALLDAY_HEIGHT) {
    825                     allDayHeight = MAX_ALLDAY_HEIGHT;
    826                 }
    827             }
    828             mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
    829         } else {
    830             mSelectionAllDay = false;
    831         }
    832         mAllDayHeight = allDayHeight;
    833 
    834         mGridAreaHeight = height - mFirstCell;
    835         mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
    836         int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
    837         int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
    838         mEventGeometry.setHourHeight(mCellHeight);
    839 
    840         // Create an off-screen bitmap that we can draw into.
    841         mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
    842         if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
    843                 mBitmapHeight > 0) {
    844             if (mBitmap != null) {
    845                 mBitmap.recycle();
    846             }
    847             mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
    848             mCanvas = new Canvas(mBitmap);
    849         }
    850         mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
    851 
    852         if (mFirstHour == -1) {
    853             initFirstHour();
    854             mFirstHourOffset = 0;
    855         }
    856 
    857         // When we change the base date, the number of all-day events may
    858         // change and that changes the cell height.  When we switch dates,
    859         // we use the mFirstHourOffset from the previous view, but that may
    860         // be too large for the new view if the cell height is smaller.
    861         if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
    862             mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
    863         }
    864         mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
    865 
    866         int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
    867         //When we get new events we don't want to dismiss the popup unless the event changes
    868         if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
    869             mPopup.dismiss();
    870         }
    871         mPopup.setWidth(eventAreaWidth - 20);
    872         mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
    873     }
    874 
    875     /**
    876      * Initialize the state for another view.  The given view is one that has
    877      * its own bitmap and will use an animation to replace the current view.
    878      * The current view and new view are either both Week views or both Day
    879      * views.  They differ in their base date.
    880      *
    881      * @param view the view to initialize.
    882      */
    883     private void initView(CalendarView view) {
    884         view.mSelectionHour = mSelectionHour;
    885         view.mSelectedEvents.clear();
    886         view.mComputeSelectedEvents = true;
    887         view.mFirstHour = mFirstHour;
    888         view.mFirstHourOffset = mFirstHourOffset;
    889         view.remeasure(getWidth(), getHeight());
    890 
    891         view.mSelectedEvent = null;
    892         view.mPrevSelectedEvent = null;
    893         view.mStartDay = mStartDay;
    894         if (view.mEvents.size() > 0) {
    895             view.mSelectionAllDay = mSelectionAllDay;
    896         } else {
    897             view.mSelectionAllDay = false;
    898         }
    899 
    900         // Redraw the screen so that the selection box will be redrawn.  We may
    901         // have scrolled to a different part of the day in some other view
    902         // so the selection box in this view may no longer be visible.
    903         view.mRedrawScreen = true;
    904         view.recalc();
    905     }
    906 
    907     /**
    908      * Switch to another view based on what was selected (an event or a free
    909      * slot) and how it was selected (by touch or by trackball).
    910      *
    911      * @param trackBallSelection true if the selection was made using the
    912      * trackball.
    913      */
    914     private void switchViews(boolean trackBallSelection) {
    915         Event selectedEvent = mSelectedEvent;
    916 
    917         mPopup.dismiss();
    918         mLastPopupEventID = INVALID_EVENT_ID;
    919         if (mNumDays > 1) {
    920             // This is the Week view.
    921             // With touch, we always switch to Day/Agenda View
    922             // With track ball, if we selected a free slot, then create an event.
    923             // If we selected a specific event, switch to EventInfo view.
    924             if (trackBallSelection) {
    925                 if (selectedEvent == null) {
    926                     // Switch to the EditEvent view
    927                     long startMillis = getSelectedTimeInMillis();
    928                     long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
    929                     Intent intent = new Intent(Intent.ACTION_VIEW);
    930                     intent.setClassName(mParentActivity, EditEvent.class.getName());
    931                     intent.putExtra(EVENT_BEGIN_TIME, startMillis);
    932                     intent.putExtra(EVENT_END_TIME, endMillis);
    933                     mParentActivity.startActivity(intent);
    934                 } else {
    935                     // Switch to the EventInfo view
    936                     Intent intent = new Intent(Intent.ACTION_VIEW);
    937                     Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
    938                             selectedEvent.id);
    939                     intent.setData(eventUri);
    940                     intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
    941                     intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
    942                     intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
    943                     mParentActivity.startActivity(intent);
    944                 }
    945             } else {
    946                 // This was a touch selection.  If the touch selected a single
    947                 // unambiguous event, then view that event.  Otherwise go to
    948                 // Day/Agenda view.
    949                 if (mSelectedEvents.size() == 1) {
    950                     // Switch to the EventInfo view
    951                     Intent intent = new Intent(Intent.ACTION_VIEW);
    952                     Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
    953                             selectedEvent.id);
    954                     intent.setData(eventUri);
    955                     intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
    956                     intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
    957                     intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
    958                     mParentActivity.startActivity(intent);
    959                 } else {
    960                     // Switch to the Day/Agenda view.
    961                     long millis = getSelectedTimeInMillis();
    962                     Utils.startActivity(mParentActivity, mDetailedView, millis);
    963                 }
    964             }
    965         } else {
    966             // This is the Day view.
    967             // If we selected a free slot, then create an event.
    968             // If we selected an event, then go to the EventInfo view.
    969             if (selectedEvent == null) {
    970                 // Switch to the EditEvent view
    971                 long startMillis = getSelectedTimeInMillis();
    972                 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
    973                 Intent intent = new Intent(Intent.ACTION_VIEW);
    974                 intent.setClassName(mParentActivity, EditEvent.class.getName());
    975                 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
    976                 intent.putExtra(EVENT_END_TIME, endMillis);
    977                 mParentActivity.startActivity(intent);
    978             } else {
    979                 // Switch to the EventInfo view
    980                 Intent intent = new Intent(Intent.ACTION_VIEW);
    981                 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
    982                 intent.setData(eventUri);
    983                 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
    984                 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
    985                 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
    986                 mParentActivity.startActivity(intent);
    987             }
    988         }
    989     }
    990 
    991     @Override
    992     public boolean onKeyUp(int keyCode, KeyEvent event) {
    993         mScrolling = false;
    994         long duration = event.getEventTime() - event.getDownTime();
    995 
    996         switch (keyCode) {
    997             case KeyEvent.KEYCODE_DPAD_CENTER:
    998                 if (mSelectionMode == SELECTION_HIDDEN) {
    999                     // Don't do anything unless the selection is visible.
   1000                     break;
   1001                 }
   1002 
   1003                 if (mSelectionMode == SELECTION_PRESSED) {
   1004                     // This was the first press when there was nothing selected.
   1005                     // Change the selection from the "pressed" state to the
   1006                     // the "selected" state.  We treat short-press and
   1007                     // long-press the same here because nothing was selected.
   1008                     mSelectionMode = SELECTION_SELECTED;
   1009                     mRedrawScreen = true;
   1010                     invalidate();
   1011                     break;
   1012                 }
   1013 
   1014                 // Check the duration to determine if this was a short press
   1015                 if (duration < ViewConfiguration.getLongPressTimeout()) {
   1016                     switchViews(true /* trackball */);
   1017                 } else {
   1018                     mSelectionMode = SELECTION_LONGPRESS;
   1019                     mRedrawScreen = true;
   1020                     invalidate();
   1021                     performLongClick();
   1022                 }
   1023                 break;
   1024             case KeyEvent.KEYCODE_BACK:
   1025                 if (event.isTracking() && !event.isCanceled()) {
   1026                     mPopup.dismiss();
   1027                     mParentActivity.finish();
   1028                     return true;
   1029                 }
   1030                 break;
   1031         }
   1032         return super.onKeyUp(keyCode, event);
   1033     }
   1034 
   1035     @Override
   1036     public boolean onKeyDown(int keyCode, KeyEvent event) {
   1037         if (mSelectionMode == SELECTION_HIDDEN) {
   1038             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
   1039                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
   1040                     || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
   1041                 // Display the selection box but don't move or select it
   1042                 // on this key press.
   1043                 mSelectionMode = SELECTION_SELECTED;
   1044                 mRedrawScreen = true;
   1045                 invalidate();
   1046                 return true;
   1047             } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
   1048                 // Display the selection box but don't select it
   1049                 // on this key press.
   1050                 mSelectionMode = SELECTION_PRESSED;
   1051                 mRedrawScreen = true;
   1052                 invalidate();
   1053                 return true;
   1054             }
   1055         }
   1056 
   1057         mSelectionMode = SELECTION_SELECTED;
   1058         mScrolling = false;
   1059         boolean redraw;
   1060         int selectionDay = mSelectionDay;
   1061 
   1062         switch (keyCode) {
   1063         case KeyEvent.KEYCODE_DEL:
   1064             // Delete the selected event, if any
   1065             Event selectedEvent = mSelectedEvent;
   1066             if (selectedEvent == null) {
   1067                 return false;
   1068             }
   1069             mPopup.dismiss();
   1070             mLastPopupEventID = INVALID_EVENT_ID;
   1071 
   1072             long begin = selectedEvent.startMillis;
   1073             long end = selectedEvent.endMillis;
   1074             long id = selectedEvent.id;
   1075             mDeleteEventHelper.delete(begin, end, id, -1);
   1076             return true;
   1077         case KeyEvent.KEYCODE_ENTER:
   1078             switchViews(true /* trackball or keyboard */);
   1079             return true;
   1080         case KeyEvent.KEYCODE_BACK:
   1081             if (event.getRepeatCount() == 0) {
   1082                 event.startTracking();
   1083                 return true;
   1084             }
   1085             return super.onKeyDown(keyCode, event);
   1086         case KeyEvent.KEYCODE_DPAD_LEFT:
   1087             if (mSelectedEvent != null) {
   1088                 mSelectedEvent = mSelectedEvent.nextLeft;
   1089             }
   1090             if (mSelectedEvent == null) {
   1091                 mLastPopupEventID = INVALID_EVENT_ID;
   1092                 selectionDay -= 1;
   1093             }
   1094             redraw = true;
   1095             break;
   1096 
   1097         case KeyEvent.KEYCODE_DPAD_RIGHT:
   1098             if (mSelectedEvent != null) {
   1099                 mSelectedEvent = mSelectedEvent.nextRight;
   1100             }
   1101             if (mSelectedEvent == null) {
   1102                 mLastPopupEventID = INVALID_EVENT_ID;
   1103                 selectionDay += 1;
   1104             }
   1105             redraw = true;
   1106             break;
   1107 
   1108         case KeyEvent.KEYCODE_DPAD_UP:
   1109             if (mSelectedEvent != null) {
   1110                 mSelectedEvent = mSelectedEvent.nextUp;
   1111             }
   1112             if (mSelectedEvent == null) {
   1113                 mLastPopupEventID = INVALID_EVENT_ID;
   1114                 if (!mSelectionAllDay) {
   1115                     mSelectionHour -= 1;
   1116                     adjustHourSelection();
   1117                     mSelectedEvents.clear();
   1118                     mComputeSelectedEvents = true;
   1119                 }
   1120             }
   1121             redraw = true;
   1122             break;
   1123 
   1124         case KeyEvent.KEYCODE_DPAD_DOWN:
   1125             if (mSelectedEvent != null) {
   1126                 mSelectedEvent = mSelectedEvent.nextDown;
   1127             }
   1128             if (mSelectedEvent == null) {
   1129                 mLastPopupEventID = INVALID_EVENT_ID;
   1130                 if (mSelectionAllDay) {
   1131                     mSelectionAllDay = false;
   1132                 } else {
   1133                     mSelectionHour++;
   1134                     adjustHourSelection();
   1135                     mSelectedEvents.clear();
   1136                     mComputeSelectedEvents = true;
   1137                 }
   1138             }
   1139             redraw = true;
   1140             break;
   1141 
   1142         default:
   1143             return super.onKeyDown(keyCode, event);
   1144         }
   1145 
   1146         if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
   1147             boolean forward;
   1148             CalendarView view = mParentActivity.getNextView();
   1149             Time date = view.mBaseDate;
   1150             date.set(mBaseDate);
   1151             if (selectionDay < mFirstJulianDay) {
   1152                 date.monthDay -= mNumDays;
   1153                 forward = false;
   1154             } else {
   1155                 date.monthDay += mNumDays;
   1156                 forward = true;
   1157             }
   1158             date.normalize(true /* ignore isDst */);
   1159             view.mSelectionDay = selectionDay;
   1160 
   1161             initView(view);
   1162             mTitleTextView.setText(view.mDateRange);
   1163             mParentActivity.switchViews(forward, 0, 0);
   1164             return true;
   1165         }
   1166         mSelectionDay = selectionDay;
   1167         mSelectedEvents.clear();
   1168         mComputeSelectedEvents = true;
   1169 
   1170         if (redraw) {
   1171             mRedrawScreen = true;
   1172             invalidate();
   1173             return true;
   1174         }
   1175 
   1176         return super.onKeyDown(keyCode, event);
   1177     }
   1178 
   1179     // This is called after scrolling stops to move the selected hour
   1180     // to the visible part of the screen.
   1181     private void resetSelectedHour() {
   1182         if (mSelectionHour < mFirstHour + 1) {
   1183             mSelectionHour = mFirstHour + 1;
   1184             mSelectedEvent = null;
   1185             mSelectedEvents.clear();
   1186             mComputeSelectedEvents = true;
   1187         } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
   1188             mSelectionHour = mFirstHour + mNumHours - 3;
   1189             mSelectedEvent = null;
   1190             mSelectedEvents.clear();
   1191             mComputeSelectedEvents = true;
   1192         }
   1193     }
   1194 
   1195     private void initFirstHour() {
   1196         mFirstHour = mSelectionHour - mNumHours / 2;
   1197         if (mFirstHour < 0) {
   1198             mFirstHour = 0;
   1199         } else if (mFirstHour + mNumHours > 24) {
   1200             mFirstHour = 24 - mNumHours;
   1201         }
   1202     }
   1203 
   1204     /**
   1205      * Recomputes the first full hour that is visible on screen after the
   1206      * screen is scrolled.
   1207      */
   1208     private void computeFirstHour() {
   1209         // Compute the first full hour that is visible on screen
   1210         mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
   1211         mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
   1212     }
   1213 
   1214     private void adjustHourSelection() {
   1215         if (mSelectionHour < 0) {
   1216             mSelectionHour = 0;
   1217             if (mMaxAllDayEvents > 0) {
   1218                 mPrevSelectedEvent = null;
   1219                 mSelectionAllDay = true;
   1220             }
   1221         }
   1222 
   1223         if (mSelectionHour > 23) {
   1224             mSelectionHour = 23;
   1225         }
   1226 
   1227         // If the selected hour is at least 2 time slots from the top and
   1228         // bottom of the screen, then don't scroll the view.
   1229         if (mSelectionHour < mFirstHour + 1) {
   1230             // If there are all-days events for the selected day but there
   1231             // are no more normal events earlier in the day, then jump to
   1232             // the all-day event area.
   1233             // Exception 1: allow the user to scroll to 8am with the trackball
   1234             // before jumping to the all-day event area.
   1235             // Exception 2: if 12am is on screen, then allow the user to select
   1236             // 12am before going up to the all-day event area.
   1237             int daynum = mSelectionDay - mFirstJulianDay;
   1238             if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
   1239                     && mFirstHour > 0 && mFirstHour < 8) {
   1240                 mPrevSelectedEvent = null;
   1241                 mSelectionAllDay = true;
   1242                 mSelectionHour = mFirstHour + 1;
   1243                 return;
   1244             }
   1245 
   1246             if (mFirstHour > 0) {
   1247                 mFirstHour -= 1;
   1248                 mViewStartY -= (mCellHeight + HOUR_GAP);
   1249                 if (mViewStartY < 0) {
   1250                     mViewStartY = 0;
   1251                 }
   1252                 return;
   1253             }
   1254         }
   1255 
   1256         if (mSelectionHour > mFirstHour + mNumHours - 3) {
   1257             if (mFirstHour < 24 - mNumHours) {
   1258                 mFirstHour += 1;
   1259                 mViewStartY += (mCellHeight + HOUR_GAP);
   1260                 if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
   1261                     mViewStartY = mBitmapHeight - mGridAreaHeight;
   1262                 }
   1263                 return;
   1264             } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
   1265                 mViewStartY = mBitmapHeight - mGridAreaHeight;
   1266             }
   1267         }
   1268     }
   1269 
   1270     void clearCachedEvents() {
   1271         mLastReloadMillis = 0;
   1272     }
   1273 
   1274     private Runnable mCancelCallback = new Runnable() {
   1275         public void run() {
   1276             clearCachedEvents();
   1277         }
   1278     };
   1279 
   1280     void reloadEvents() {
   1281         // Protect against this being called before this view has been
   1282         // initialized.
   1283         if (mParentActivity == null) {
   1284             return;
   1285         }
   1286 
   1287         mSelectedEvent = null;
   1288         mPrevSelectedEvent = null;
   1289         mSelectedEvents.clear();
   1290 
   1291         // The start date is the beginning of the week at 12am
   1292         Time weekStart = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
   1293         weekStart.set(mBaseDate);
   1294         weekStart.hour = 0;
   1295         weekStart.minute = 0;
   1296         weekStart.second = 0;
   1297         long millis = weekStart.normalize(true /* ignore isDst */);
   1298 
   1299         // Avoid reloading events unnecessarily.
   1300         if (millis == mLastReloadMillis) {
   1301             return;
   1302         }
   1303         mLastReloadMillis = millis;
   1304 
   1305         // load events in the background
   1306         mParentActivity.startProgressSpinner();
   1307         final ArrayList<Event> events = new ArrayList<Event>();
   1308         mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
   1309             public void run() {
   1310                 mEvents = events;
   1311                 mRemeasure = true;
   1312                 mRedrawScreen = true;
   1313                 mComputeSelectedEvents = true;
   1314                 recalc();
   1315                 mParentActivity.stopProgressSpinner();
   1316                 invalidate();
   1317             }
   1318         }, mCancelCallback);
   1319     }
   1320 
   1321     @Override
   1322     protected void onDraw(Canvas canvas) {
   1323         if (mRemeasure) {
   1324             remeasure(getWidth(), getHeight());
   1325             mRemeasure = false;
   1326         }
   1327 
   1328         if (mRedrawScreen && mCanvas != null) {
   1329             doDraw(mCanvas);
   1330             mRedrawScreen = false;
   1331         }
   1332 
   1333         if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
   1334             canvas.save();
   1335             if (mViewStartX > 0) {
   1336                 canvas.translate(mViewWidth - mViewStartX, 0);
   1337             } else {
   1338                 canvas.translate(-(mViewWidth + mViewStartX), 0);
   1339             }
   1340             CalendarView nextView = mParentActivity.getNextView();
   1341 
   1342             // Prevent infinite recursive calls to onDraw().
   1343             nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
   1344 
   1345             nextView.onDraw(canvas);
   1346             canvas.restore();
   1347             canvas.save();
   1348             canvas.translate(-mViewStartX, 0);
   1349         }
   1350 
   1351         if (mBitmap != null) {
   1352             drawCalendarView(canvas);
   1353         }
   1354 
   1355         // Draw the fixed areas (that don't scroll) directly to the canvas.
   1356         drawAfterScroll(canvas);
   1357         mComputeSelectedEvents = false;
   1358 
   1359         if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
   1360             canvas.restore();
   1361         }
   1362 
   1363         sendAccessibilityEvents();
   1364     }
   1365 
   1366     private void drawCalendarView(Canvas canvas) {
   1367 
   1368         // Copy the scrollable region from the big bitmap to the canvas.
   1369         Rect src = mSrcRect;
   1370         Rect dest = mDestRect;
   1371 
   1372         src.top = mViewStartY;
   1373         src.bottom = mViewStartY + mGridAreaHeight;
   1374         src.left = 0;
   1375         src.right = mViewWidth;
   1376 
   1377         dest.top = mFirstCell;
   1378         dest.bottom = mViewHeight;
   1379         dest.left = 0;
   1380         dest.right = mViewWidth;
   1381 
   1382         canvas.save();
   1383         canvas.clipRect(dest);
   1384         canvas.drawColor(0, PorterDuff.Mode.CLEAR);
   1385         canvas.drawBitmap(mBitmap, src, dest, null);
   1386         canvas.restore();
   1387     }
   1388 
   1389     private void drawAfterScroll(Canvas canvas) {
   1390         Paint p = mPaint;
   1391         Rect r = mRect;
   1392 
   1393         if (mMaxAllDayEvents != 0) {
   1394             drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
   1395             drawUpperLeftCorner(r, canvas, p);
   1396         }
   1397 
   1398         if (mNumDays > 1) {
   1399             drawDayHeaderLoop(r, canvas, p);
   1400         }
   1401 
   1402         // Draw the AM and PM indicators if we're in 12 hour mode
   1403         if (!mIs24HourFormat) {
   1404             drawAmPm(canvas, p);
   1405         }
   1406 
   1407         // Update the popup window showing the event details, but only if
   1408         // we are not scrolling and we have focus.
   1409         if (!mScrolling && isFocused()) {
   1410             updateEventDetails();
   1411         }
   1412     }
   1413 
   1414     // This isn't really the upper-left corner.  It's the square area just
   1415     // below the upper-left corner, above the hours and to the left of the
   1416     // all-day area.
   1417     private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
   1418         p.setColor(mCalendarHourBackground);
   1419         r.top = mBannerPlusMargin;
   1420         r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
   1421         r.left = 0;
   1422         r.right = mHoursWidth;
   1423         canvas.drawRect(r, p);
   1424     }
   1425 
   1426     private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
   1427         // Draw the horizontal day background banner
   1428         p.setColor(mCalendarDateBannerBackground);
   1429         r.top = 0;
   1430         r.bottom = mBannerPlusMargin;
   1431         r.left = 0;
   1432         r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
   1433         canvas.drawRect(r, p);
   1434 
   1435         // Fill the extra space on the right side with the default background
   1436         r.left = r.right;
   1437         r.right = mViewWidth;
   1438         p.setColor(mCalendarGridAreaBackground);
   1439         canvas.drawRect(r, p);
   1440 
   1441         // Draw a highlight on the selected day (if any), but only if we are
   1442         // displaying more than one day.
   1443         if (mSelectionMode != SELECTION_HIDDEN) {
   1444             if (mNumDays > 1) {
   1445                 p.setColor(mCalendarDateSelected);
   1446                 r.top = 0;
   1447                 r.bottom = mBannerPlusMargin;
   1448                 int daynum = mSelectionDay - mFirstJulianDay;
   1449                 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
   1450                 r.right = r.left + mCellWidth;
   1451                 canvas.drawRect(r, p);
   1452             }
   1453         }
   1454 
   1455         p.setTextSize(NORMAL_FONT_SIZE);
   1456         p.setTextAlign(Paint.Align.CENTER);
   1457         int x = mHoursWidth;
   1458         int deltaX = mCellWidth + DAY_GAP;
   1459         int cell = mFirstJulianDay;
   1460 
   1461         String[] dayNames;
   1462         if (mDateStrWidth < mCellWidth) {
   1463             dayNames = mDayStrs;
   1464         } else {
   1465             dayNames = mDayStrs2Letter;
   1466         }
   1467 
   1468         p.setTypeface(mBold);
   1469         p.setAntiAlias(true);
   1470         for (int day = 0; day < mNumDays; day++, cell++) {
   1471             drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
   1472             x += deltaX;
   1473         }
   1474     }
   1475 
   1476     private void drawAmPm(Canvas canvas, Paint p) {
   1477         p.setColor(mCalendarAmPmLabel);
   1478         p.setTextSize(AMPM_FONT_SIZE);
   1479         p.setTypeface(mBold);
   1480         p.setAntiAlias(true);
   1481         mPaint.setTextAlign(Paint.Align.RIGHT);
   1482         String text = mAmString;
   1483         if (mFirstHour >= 12) {
   1484             text = mPmString;
   1485         }
   1486         int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
   1487         int right = mHoursWidth - HOURS_RIGHT_MARGIN;
   1488         canvas.drawText(text, right, y, p);
   1489 
   1490         if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
   1491             // Also draw the "PM"
   1492             text = mPmString;
   1493             y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
   1494                     + 2 * mHoursTextHeight + HOUR_GAP;
   1495             canvas.drawText(text, right, y, p);
   1496         }
   1497     }
   1498 
   1499     private void drawCurrentTimeMarker(int top, Canvas canvas, Paint p) {
   1500         Rect r = new Rect();
   1501         r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
   1502         r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
   1503         r.left = 0;
   1504         r.right = mHoursWidth;
   1505 
   1506         p.setColor(mCurrentTimeMarkerColor);
   1507         canvas.drawRect(r, p);
   1508     }
   1509 
   1510     private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) {
   1511         //Do a white outline so it'll show up on a red event
   1512         p.setColor(mCurrentTimeMarkerBorderColor);
   1513         r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH;
   1514         r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2 + CURRENT_TIME_LINE_BORDER_WIDTH;
   1515         r.left = left + CURRENT_TIME_LINE_SIDE_BUFFER;
   1516         r.right = r.left + mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER;
   1517         canvas.drawRect(r, p);
   1518         //Then draw the red line
   1519         p.setColor(mCurrentTimeLineColor);
   1520         r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
   1521         r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
   1522         canvas.drawRect(r, p);
   1523     }
   1524 
   1525     private void doDraw(Canvas canvas) {
   1526         Paint p = mPaint;
   1527         Rect r = mRect;
   1528         int lineY = mCurrentTime.hour*(mCellHeight + HOUR_GAP)
   1529             + ((mCurrentTime.minute * mCellHeight) / 60)
   1530             + 1;
   1531 
   1532         drawGridBackground(r, canvas, p);
   1533         drawHours(r, canvas, p);
   1534 
   1535         // Draw each day
   1536         int x = mHoursWidth;
   1537         int deltaX = mCellWidth + DAY_GAP;
   1538         int cell = mFirstJulianDay;
   1539         for (int day = 0; day < mNumDays; day++, cell++) {
   1540             drawEvents(cell, x, HOUR_GAP, canvas, p);
   1541             //If this is today
   1542             if(cell == mTodayJulianDay) {
   1543                 //And the current time shows up somewhere on the screen
   1544                 if(lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
   1545                     //draw both the marker and the line
   1546                     drawCurrentTimeMarker(lineY, canvas, p);
   1547                     drawCurrentTimeLine(r, x, lineY, canvas, p);
   1548                 }
   1549             }
   1550             x += deltaX;
   1551         }
   1552     }
   1553 
   1554     private void drawHours(Rect r, Canvas canvas, Paint p) {
   1555         // Draw the background for the hour labels
   1556         p.setColor(mCalendarHourBackground);
   1557         r.top = 0;
   1558         r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
   1559         r.left = 0;
   1560         r.right = mHoursWidth;
   1561         canvas.drawRect(r, p);
   1562 
   1563         // Fill the bottom left corner with the default grid background
   1564         r.top = r.bottom;
   1565         r.bottom = mBitmapHeight;
   1566         p.setColor(mCalendarGridAreaBackground);
   1567         canvas.drawRect(r, p);
   1568 
   1569         // Draw a highlight on the selected hour (if needed)
   1570         if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
   1571             p.setColor(mCalendarHourSelected);
   1572             r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
   1573             r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
   1574             r.left = 0;
   1575             r.right = mHoursWidth;
   1576             canvas.drawRect(r, p);
   1577 
   1578             boolean drawBorder = false;
   1579             if (!drawBorder) {
   1580                 r.top += HOUR_GAP;
   1581                 r.bottom -= HOUR_GAP;
   1582             }
   1583 
   1584             // Also draw the highlight on the grid
   1585             p.setColor(mCalendarGridAreaSelected);
   1586             int daynum = mSelectionDay - mFirstJulianDay;
   1587             r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
   1588             r.right = r.left + mCellWidth;
   1589             canvas.drawRect(r, p);
   1590 
   1591             // Draw a border around the highlighted grid hour.
   1592             if (drawBorder) {
   1593                 Path path = mPath;
   1594                 r.top += HOUR_GAP;
   1595                 r.bottom -= HOUR_GAP;
   1596                 path.reset();
   1597                 path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
   1598                 canvas.drawPath(path, mSelectionPaint);
   1599             }
   1600 
   1601             saveSelectionPosition(r.left, r.top, r.right, r.bottom);
   1602         }
   1603 
   1604         p.setColor(mCalendarHourLabel);
   1605         p.setTextSize(HOURS_FONT_SIZE);
   1606         p.setTypeface(mBold);
   1607         p.setTextAlign(Paint.Align.RIGHT);
   1608         p.setAntiAlias(true);
   1609 
   1610         int right = mHoursWidth - HOURS_RIGHT_MARGIN;
   1611         int y = HOUR_GAP + mHoursTextHeight;
   1612 
   1613         for (int i = 0; i < 24; i++) {
   1614             String time = mHourStrs[i];
   1615             canvas.drawText(time, right, y, p);
   1616             y += mCellHeight + HOUR_GAP;
   1617         }
   1618     }
   1619 
   1620     private void sendAccessibilityEvents() {
   1621         if (!isShown() || !AccessibilityManager.getInstance(mContext).isEnabled()) {
   1622             return;
   1623         }
   1624         // if the title text has changed => announce period
   1625         CharSequence titleTextViewText = mTitleTextView.getText();
   1626         // intended use of identity comparison
   1627         boolean titleChanged = titleTextViewText != mPrevTitleTextViewText;
   1628         if (titleChanged) {
   1629             mPrevTitleTextViewText = titleTextViewText;
   1630             sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
   1631         }
   1632         // if title or selection has changed => announce selection
   1633         // Note: if the title has changed we want to send both events
   1634         if (titleChanged || mPrevSelectionDay != mSelectionDay
   1635                 || mPrevSelectionHour != mSelectionHour) {
   1636             mPrevSelectionDay = mSelectionDay;
   1637             mPrevSelectionHour = mSelectionHour;
   1638             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
   1639         }
   1640     }
   1641 
   1642     @Override
   1643     public void sendAccessibilityEvent(int eventType) {
   1644         // we send only selection events since semantically we select
   1645         // certain element and not always this view gets focus which
   1646         // triggers firing of a focus accessibility event
   1647         if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
   1648             return;
   1649         }
   1650         super.sendAccessibilityEvent(eventType);
   1651     }
   1652 
   1653     @Override
   1654     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
   1655         if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
   1656             // add the currently shown period (day/week)
   1657             if (mNumDays == 1) {
   1658                 // for daily view the title has enough context information
   1659                 event.getText().add(mTitleTextView.getText());
   1660             } else {
   1661                 // since the title view does not contain enough context we
   1662                 // compute a more descriptive title for the shown time frame
   1663                 int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH
   1664                         | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
   1665                         | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   1666                 if (DateFormat.is24HourFormat(mParentActivity)) {
   1667                     flags |= DateUtils.FORMAT_24HOUR;
   1668                 }
   1669 
   1670                 long start = mBaseDate.toMillis(false);
   1671                 long gmtOff = mBaseDate.gmtoff;
   1672                 int firstJulianDay = Time.getJulianDay(start, gmtOff);
   1673 
   1674                 Time time = new Time(mBaseDate);
   1675                 time.setJulianDay(firstJulianDay);
   1676                 long startTime = time.normalize(true);
   1677                 time.setJulianDay(firstJulianDay + mNumDays);
   1678                 long endTime = time.normalize(true);
   1679 
   1680                 String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
   1681                         flags);
   1682                 event.getText().add(timeRange);
   1683             }
   1684         } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
   1685             int flags = 0;
   1686             // add the selection
   1687             if (mNumDays == 1) {
   1688                 // if day view we need only hour information
   1689                 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   1690             } else {
   1691                 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
   1692                         | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   1693             }
   1694             long startTime = getSelectedTimeInMillis();
   1695             long endTime = startTime + MILLIS_PER_HOUR;
   1696             if (DateFormat.is24HourFormat(mParentActivity)) {
   1697                 flags |= DateUtils.FORMAT_24HOUR;
   1698             }
   1699             String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
   1700                     flags);
   1701             event.getText().add(timeRange);
   1702 
   1703             // add the selected event data if such
   1704             if (mSelectedEvent != null) {
   1705                 Event selectedEvent = mSelectedEvent;
   1706                 if (mTempEventBundle == null) {
   1707                     mTempEventBundle = new Bundle();
   1708                 }
   1709                 Bundle bundle = mTempEventBundle;
   1710                 bundle.clear();
   1711                 bundle.putLong("id", selectedEvent.id);
   1712                 bundle.putInt("color", selectedEvent.color);
   1713                 bundle.putCharSequence("title", selectedEvent.title);
   1714                 bundle.putCharSequence("location", selectedEvent.location);
   1715                 bundle.putBoolean("allDay", selectedEvent.allDay);
   1716                 bundle.putInt("startDay", selectedEvent.startDay);
   1717                 bundle.putInt("endDay", selectedEvent.endDay);
   1718                 bundle.putInt("startTime", selectedEvent.startTime);
   1719                 bundle.putInt("endTime", selectedEvent.endTime);
   1720                 bundle.putLong("startMillis", selectedEvent.startMillis);
   1721                 bundle.putLong("endMillis", selectedEvent.endMillis);
   1722                 bundle.putString("organizer", selectedEvent.organizer);
   1723                 bundle.putBoolean("guestsCanModify", selectedEvent.guestsCanModify);
   1724                 event.setParcelableData(bundle);
   1725             }
   1726         }
   1727 
   1728         // add day event count, events for same hour count and
   1729         // the index of the selected event for the same hour
   1730         int todayEventCount = 0;
   1731         int sameHourEventCount = 0;
   1732         int currentSameHourEventIndex = 0;
   1733         int selectionHourStart = mSelectionHour * MINUTES_PER_HOUR;
   1734         int selectionHourEnd = selectionHourStart + MINUTES_PER_HOUR;
   1735         for (int i = 0, count = mEvents.size(); i < count; i++) {
   1736             Event calendarEvent = mEvents.get(i);
   1737             if (calendarEvent.endDay == mSelectionDay) {
   1738                 todayEventCount++;
   1739                 if (selectionHourStart >= calendarEvent.endTime
   1740                         || selectionHourEnd <= calendarEvent.startTime) {
   1741                     continue;
   1742                 }
   1743                 if (calendarEvent == mSelectedEvent) {
   1744                     currentSameHourEventIndex = sameHourEventCount;
   1745                 }
   1746                 sameHourEventCount++;
   1747             }
   1748         }
   1749         event.setAddedCount(todayEventCount);
   1750         event.setItemCount(sameHourEventCount);
   1751         event.setCurrentItemIndex(currentSameHourEventIndex);
   1752 
   1753         return true;
   1754     }
   1755 
   1756     private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
   1757         float xCenter = x + mCellWidth / 2.0f;
   1758 
   1759         if (Utils.isSaturday(day, mStartDay)) {
   1760             p.setColor(mWeek_saturdayColor);
   1761         } else if (Utils.isSunday(day, mStartDay)) {
   1762             p.setColor(mWeek_sundayColor);
   1763         } else {
   1764             p.setColor(mCalendarDateBannerTextColor);
   1765         }
   1766 
   1767         int dateNum = mFirstDate + day;
   1768         if (dateNum > mMonthLength) {
   1769             dateNum -= mMonthLength;
   1770         }
   1771 
   1772         String dateNumStr;
   1773         // Add a leading zero if the date is a single digit
   1774         if (dateNum < 10) {
   1775             dateNumStr = "0" + dateNum;
   1776         } else {
   1777             dateNumStr = String.valueOf(dateNum);
   1778         }
   1779 
   1780         DayHeader header = dayHeaders[day];
   1781         if (header == null || header.cell != cell) {
   1782             // The day header string is regenerated on every draw during drag and fling animation.
   1783             // Caching day header since formatting the string takes surprising long time.
   1784 
   1785             dayHeaders[day] = new DayHeader();
   1786             dayHeaders[day].cell = cell;
   1787             dayHeaders[day].dateString = getResources().getString(
   1788                     R.string.weekday_day, dateStr, dateNumStr);
   1789         }
   1790         dateStr = dayHeaders[day].dateString;
   1791 
   1792         float y = mBannerPlusMargin - 7;
   1793         canvas.drawText(dateStr, xCenter, y, p);
   1794     }
   1795 
   1796     private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
   1797         Paint.Style savedStyle = p.getStyle();
   1798 
   1799         // Clear the background
   1800         p.setColor(mCalendarGridAreaBackground);
   1801         r.top = 0;
   1802         r.bottom = mBitmapHeight;
   1803         r.left = 0;
   1804         r.right = mViewWidth;
   1805         canvas.drawRect(r, p);
   1806 
   1807         // Draw the horizontal grid lines
   1808         p.setColor(mCalendarGridLineHorizontalColor);
   1809         p.setStyle(Style.STROKE);
   1810         p.setStrokeWidth(0);
   1811         p.setAntiAlias(false);
   1812         float startX = mHoursWidth;
   1813         float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
   1814         float y = 0;
   1815         float deltaY = mCellHeight + HOUR_GAP;
   1816         for (int hour = 0; hour <= 24; hour++) {
   1817             canvas.drawLine(startX, y, stopX, y, p);
   1818             y += deltaY;
   1819         }
   1820 
   1821         // Draw the vertical grid lines
   1822         p.setColor(mCalendarGridLineVerticalColor);
   1823         float startY = 0;
   1824         float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
   1825         float deltaX = mCellWidth + DAY_GAP;
   1826         float x = mHoursWidth + mCellWidth;
   1827         for (int day = 0; day < mNumDays; day++) {
   1828             canvas.drawLine(x, startY, x, stopY, p);
   1829             x += deltaX;
   1830         }
   1831 
   1832         // Restore the saved style.
   1833         p.setStyle(savedStyle);
   1834         p.setAntiAlias(true);
   1835     }
   1836 
   1837     Event getSelectedEvent() {
   1838         if (mSelectedEvent == null) {
   1839             // There is no event at the selected hour, so create a new event.
   1840             return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
   1841                     getSelectedMinutesSinceMidnight());
   1842         }
   1843         return mSelectedEvent;
   1844     }
   1845 
   1846     boolean isEventSelected() {
   1847         return (mSelectedEvent != null);
   1848     }
   1849 
   1850     Event getNewEvent() {
   1851         return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
   1852                 getSelectedMinutesSinceMidnight());
   1853     }
   1854 
   1855     static Event getNewEvent(int julianDay, long utcMillis,
   1856             int minutesSinceMidnight) {
   1857         Event event = Event.newInstance();
   1858         event.startDay = julianDay;
   1859         event.endDay = julianDay;
   1860         event.startMillis = utcMillis;
   1861         event.endMillis = event.startMillis + MILLIS_PER_HOUR;
   1862         event.startTime = minutesSinceMidnight;
   1863         event.endTime = event.startTime + MINUTES_PER_HOUR;
   1864         return event;
   1865     }
   1866 
   1867     private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
   1868         float maxWidthF = 0.0f;
   1869 
   1870         int len = strings.length;
   1871         for (int i = 0; i < len; i++) {
   1872             float width = p.measureText(strings[i]);
   1873             maxWidthF = Math.max(width, maxWidthF);
   1874         }
   1875         int maxWidth = (int) (maxWidthF + 0.5);
   1876         if (maxWidth < currentMax) {
   1877             maxWidth = currentMax;
   1878         }
   1879         return maxWidth;
   1880     }
   1881 
   1882     private void saveSelectionPosition(float left, float top, float right, float bottom) {
   1883         mPrevBox.left = (int) left;
   1884         mPrevBox.right = (int) right;
   1885         mPrevBox.top = (int) top;
   1886         mPrevBox.bottom = (int) bottom;
   1887     }
   1888 
   1889     private Rect getCurrentSelectionPosition() {
   1890         Rect box = new Rect();
   1891         box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
   1892         box.bottom = box.top + mCellHeight + HOUR_GAP;
   1893         int daynum = mSelectionDay - mFirstJulianDay;
   1894         box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
   1895         box.right = box.left + mCellWidth + DAY_GAP;
   1896         return box;
   1897     }
   1898 
   1899     private void drawAllDayEvents(int firstDay, int numDays,
   1900             Rect r, Canvas canvas, Paint p) {
   1901         p.setTextSize(NORMAL_FONT_SIZE);
   1902         p.setTextAlign(Paint.Align.LEFT);
   1903         Paint eventTextPaint = mEventTextPaint;
   1904 
   1905         // Draw the background for the all-day events area
   1906         r.top = mBannerPlusMargin;
   1907         r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
   1908         r.left = mHoursWidth;
   1909         r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
   1910         p.setColor(mCalendarAllDayBackground);
   1911         canvas.drawRect(r, p);
   1912 
   1913         // Fill the extra space on the right side with the default background
   1914         r.left = r.right;
   1915         r.right = mViewWidth;
   1916         p.setColor(mCalendarGridAreaBackground);
   1917         canvas.drawRect(r, p);
   1918 
   1919         // Draw the vertical grid lines
   1920         p.setColor(mCalendarGridLineVerticalColor);
   1921         p.setStyle(Style.STROKE);
   1922         p.setStrokeWidth(0);
   1923         p.setAntiAlias(false);
   1924         float startY = r.top;
   1925         float stopY = r.bottom;
   1926         float deltaX = mCellWidth + DAY_GAP;
   1927         float x = mHoursWidth + mCellWidth;
   1928         for (int day = 0; day <= mNumDays; day++) {
   1929             canvas.drawLine(x, startY, x, stopY, p);
   1930             x += deltaX;
   1931         }
   1932         p.setAntiAlias(true);
   1933         p.setStyle(Style.FILL);
   1934 
   1935         int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
   1936         float left = mHoursWidth;
   1937         int lastDay = firstDay + numDays - 1;
   1938         ArrayList<Event> events = mEvents;
   1939         int numEvents = events.size();
   1940         float drawHeight = mAllDayHeight;
   1941         float numRectangles = mMaxAllDayEvents;
   1942         for (int i = 0; i < numEvents; i++) {
   1943             Event event = events.get(i);
   1944             if (!event.allDay)
   1945                 continue;
   1946             int startDay = event.startDay;
   1947             int endDay = event.endDay;
   1948             if (startDay > lastDay || endDay < firstDay)
   1949                 continue;
   1950             if (startDay < firstDay)
   1951                 startDay = firstDay;
   1952             if (endDay > lastDay)
   1953                 endDay = lastDay;
   1954             int startIndex = startDay - firstDay;
   1955             int endIndex = endDay - firstDay;
   1956             float height = drawHeight / numRectangles;
   1957 
   1958             // Prevent a single event from getting too big
   1959             if (height > MAX_ALLDAY_EVENT_HEIGHT) {
   1960                 height = MAX_ALLDAY_EVENT_HEIGHT;
   1961             }
   1962 
   1963             // Leave a one-pixel space between the vertical day lines and the
   1964             // event rectangle.
   1965             event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
   1966             event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
   1967             event.top = y + height * event.getColumn();
   1968 
   1969             // Multiply the height by 0.9 to leave a little gap between events
   1970             event.bottom = event.top + height * 0.9f;
   1971 
   1972             RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
   1973             drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
   1974 
   1975             // Check if this all-day event intersects the selected day
   1976             if (mSelectionAllDay && mComputeSelectedEvents) {
   1977                 if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
   1978                     mSelectedEvents.add(event);
   1979                 }
   1980             }
   1981         }
   1982 
   1983         if (mSelectionAllDay) {
   1984             // Compute the neighbors for the list of all-day events that
   1985             // intersect the selected day.
   1986             computeAllDayNeighbors();
   1987             if (mSelectedEvent != null) {
   1988                 Event event = mSelectedEvent;
   1989                 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
   1990                 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
   1991             }
   1992 
   1993             // Draw the highlight on the selected all-day area
   1994             float top = mBannerPlusMargin + 1;
   1995             float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
   1996             int daynum = mSelectionDay - mFirstJulianDay;
   1997             left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
   1998             float right = left + mCellWidth + DAY_GAP - 1;
   1999             if (mNumDays == 1) {
   2000                 // The Day view doesn't have a vertical line on the right.
   2001                 right -= 1;
   2002             }
   2003             Path path = mPath;
   2004             path.reset();
   2005             path.addRect(left, top, right, bottom, Direction.CW);
   2006             canvas.drawPath(path, mSelectionPaint);
   2007 
   2008             // Set the selection position to zero so that when we move down
   2009             // to the normal event area, we will highlight the topmost event.
   2010             saveSelectionPosition(0f, 0f, 0f, 0f);
   2011         }
   2012     }
   2013 
   2014     private void computeAllDayNeighbors() {
   2015         int len = mSelectedEvents.size();
   2016         if (len == 0 || mSelectedEvent != null) {
   2017             return;
   2018         }
   2019 
   2020         // First, clear all the links
   2021         for (int ii = 0; ii < len; ii++) {
   2022             Event ev = mSelectedEvents.get(ii);
   2023             ev.nextUp = null;
   2024             ev.nextDown = null;
   2025             ev.nextLeft = null;
   2026             ev.nextRight = null;
   2027         }
   2028 
   2029         // For each event in the selected event list "mSelectedEvents", find
   2030         // its neighbors in the up and down directions.  This could be done
   2031         // more efficiently by sorting on the Event.getColumn() field, but
   2032         // the list is expected to be very small.
   2033 
   2034         // Find the event in the same row as the previously selected all-day
   2035         // event, if any.
   2036         int startPosition = -1;
   2037         if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
   2038             startPosition = mPrevSelectedEvent.getColumn();
   2039         }
   2040         int maxPosition = -1;
   2041         Event startEvent = null;
   2042         Event maxPositionEvent = null;
   2043         for (int ii = 0; ii < len; ii++) {
   2044             Event ev = mSelectedEvents.get(ii);
   2045             int position = ev.getColumn();
   2046             if (position == startPosition) {
   2047                 startEvent = ev;
   2048             } else if (position > maxPosition) {
   2049                 maxPositionEvent = ev;
   2050                 maxPosition = position;
   2051             }
   2052             for (int jj = 0; jj < len; jj++) {
   2053                 if (jj == ii) {
   2054                     continue;
   2055                 }
   2056                 Event neighbor = mSelectedEvents.get(jj);
   2057                 int neighborPosition = neighbor.getColumn();
   2058                 if (neighborPosition == position - 1) {
   2059                     ev.nextUp = neighbor;
   2060                 } else if (neighborPosition == position + 1) {
   2061                     ev.nextDown = neighbor;
   2062                 }
   2063             }
   2064         }
   2065         if (startEvent != null) {
   2066             mSelectedEvent = startEvent;
   2067         } else {
   2068             mSelectedEvent = maxPositionEvent;
   2069         }
   2070     }
   2071 
   2072     RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
   2073         // If this event is selected, then use the selection color
   2074         if (mSelectedEvent == event) {
   2075             // Also, remember the last selected event that we drew
   2076             mPrevSelectedEvent = event;
   2077             p.setColor(mSelectionColor);
   2078             eventTextPaint.setColor(mSelectedEventTextColor);
   2079         } else {
   2080             // Use the normal color for all-day events
   2081             p.setColor(event.color);
   2082             eventTextPaint.setColor(mEventTextColor);
   2083         }
   2084 
   2085         RectF rf = mRectF;
   2086         rf.top = event.top;
   2087         rf.bottom = event.bottom;
   2088         rf.left = event.left;
   2089         rf.right = event.right;
   2090         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
   2091 
   2092         rf.left += 2;
   2093         rf.right -= 2;
   2094         return rf;
   2095     }
   2096 
   2097     private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
   2098         Paint eventTextPaint = mEventTextPaint;
   2099         int cellWidth = mCellWidth;
   2100         int cellHeight = mCellHeight;
   2101 
   2102         // Use the selected hour as the selection region
   2103         Rect selectionArea = mRect;
   2104         selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
   2105         selectionArea.bottom = selectionArea.top + cellHeight;
   2106         selectionArea.left = left;
   2107         selectionArea.right = selectionArea.left + cellWidth;
   2108 
   2109         ArrayList<Event> events = mEvents;
   2110         int numEvents = events.size();
   2111         EventGeometry geometry = mEventGeometry;
   2112 
   2113         for (int i = 0; i < numEvents; i++) {
   2114             Event event = events.get(i);
   2115             if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
   2116                 continue;
   2117             }
   2118 
   2119             if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
   2120                     && geometry.eventIntersectsSelection(event, selectionArea)) {
   2121                 mSelectedEvents.add(event);
   2122             }
   2123 
   2124             RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
   2125             drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
   2126         }
   2127 
   2128         if (date == mSelectionDay && !mSelectionAllDay && isFocused()
   2129                 && mSelectionMode != SELECTION_HIDDEN) {
   2130             computeNeighbors();
   2131             if (mSelectedEvent != null) {
   2132                 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
   2133                 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
   2134             }
   2135         }
   2136     }
   2137 
   2138     // Computes the "nearest" neighbor event in four directions (left, right,
   2139     // up, down) for each of the events in the mSelectedEvents array.
   2140     private void computeNeighbors() {
   2141         int len = mSelectedEvents.size();
   2142         if (len == 0 || mSelectedEvent != null) {
   2143             return;
   2144         }
   2145 
   2146         // First, clear all the links
   2147         for (int ii = 0; ii < len; ii++) {
   2148             Event ev = mSelectedEvents.get(ii);
   2149             ev.nextUp = null;
   2150             ev.nextDown = null;
   2151             ev.nextLeft = null;
   2152             ev.nextRight = null;
   2153         }
   2154 
   2155         Event startEvent = mSelectedEvents.get(0);
   2156         int startEventDistance1 = 100000;  // any large number
   2157         int startEventDistance2 = 100000;  // any large number
   2158         int prevLocation = FROM_NONE;
   2159         int prevTop;
   2160         int prevBottom;
   2161         int prevLeft;
   2162         int prevRight;
   2163         int prevCenter = 0;
   2164         Rect box = getCurrentSelectionPosition();
   2165         if (mPrevSelectedEvent != null) {
   2166             prevTop = (int) mPrevSelectedEvent.top;
   2167             prevBottom = (int) mPrevSelectedEvent.bottom;
   2168             prevLeft = (int) mPrevSelectedEvent.left;
   2169             prevRight = (int) mPrevSelectedEvent.right;
   2170             // Check if the previously selected event intersects the previous
   2171             // selection box.  (The previously selected event may be from a
   2172             // much older selection box.)
   2173             if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
   2174                     || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
   2175                 mPrevSelectedEvent = null;
   2176                 prevTop = mPrevBox.top;
   2177                 prevBottom = mPrevBox.bottom;
   2178                 prevLeft = mPrevBox.left;
   2179                 prevRight = mPrevBox.right;
   2180             } else {
   2181                 // Clip the top and bottom to the previous selection box.
   2182                 if (prevTop < mPrevBox.top) {
   2183                     prevTop = mPrevBox.top;
   2184                 }
   2185                 if (prevBottom > mPrevBox.bottom) {
   2186                     prevBottom = mPrevBox.bottom;
   2187                 }
   2188             }
   2189         } else {
   2190             // Just use the previously drawn selection box
   2191             prevTop = mPrevBox.top;
   2192             prevBottom = mPrevBox.bottom;
   2193             prevLeft = mPrevBox.left;
   2194             prevRight = mPrevBox.right;
   2195         }
   2196 
   2197         // Figure out where we came from and compute the center of that area.
   2198         if (prevLeft >= box.right) {
   2199             // The previously selected event was to the right of us.
   2200             prevLocation = FROM_RIGHT;
   2201             prevCenter = (prevTop + prevBottom) / 2;
   2202         } else if (prevRight <= box.left) {
   2203             // The previously selected event was to the left of us.
   2204             prevLocation = FROM_LEFT;
   2205             prevCenter = (prevTop + prevBottom) / 2;
   2206         } else if (prevBottom <= box.top) {
   2207             // The previously selected event was above us.
   2208             prevLocation = FROM_ABOVE;
   2209             prevCenter = (prevLeft + prevRight) / 2;
   2210         } else if (prevTop >= box.bottom) {
   2211             // The previously selected event was below us.
   2212             prevLocation = FROM_BELOW;
   2213             prevCenter = (prevLeft + prevRight) / 2;
   2214         }
   2215 
   2216         // For each event in the selected event list "mSelectedEvents", search
   2217         // all the other events in that list for the nearest neighbor in 4
   2218         // directions.
   2219         for (int ii = 0; ii < len; ii++) {
   2220             Event ev = mSelectedEvents.get(ii);
   2221 
   2222             int startTime = ev.startTime;
   2223             int endTime = ev.endTime;
   2224             int left = (int) ev.left;
   2225             int right = (int) ev.right;
   2226             int top = (int) ev.top;
   2227             if (top < box.top) {
   2228                 top = box.top;
   2229             }
   2230             int bottom = (int) ev.bottom;
   2231             if (bottom > box.bottom) {
   2232                 bottom = box.bottom;
   2233             }
   2234             if (false) {
   2235                 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
   2236                         | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   2237                 if (DateFormat.is24HourFormat(mParentActivity)) {
   2238                     flags |= DateUtils.FORMAT_24HOUR;
   2239                 }
   2240                 String timeRange = Utils.formatDateRange(mParentActivity,
   2241                         ev.startMillis, ev.endMillis, flags);
   2242                 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
   2243                         + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
   2244             }
   2245             int upDistanceMin = 10000;     // any large number
   2246             int downDistanceMin = 10000;   // any large number
   2247             int leftDistanceMin = 10000;   // any large number
   2248             int rightDistanceMin = 10000;  // any large number
   2249             Event upEvent = null;
   2250             Event downEvent = null;
   2251             Event leftEvent = null;
   2252             Event rightEvent = null;
   2253 
   2254             // Pick the starting event closest to the previously selected event,
   2255             // if any.  distance1 takes precedence over distance2.
   2256             int distance1 = 0;
   2257             int distance2 = 0;
   2258             if (prevLocation == FROM_ABOVE) {
   2259                 if (left >= prevCenter) {
   2260                     distance1 = left - prevCenter;
   2261                 } else if (right <= prevCenter) {
   2262                     distance1 = prevCenter - right;
   2263                 }
   2264                 distance2 = top - prevBottom;
   2265             } else if (prevLocation == FROM_BELOW) {
   2266                 if (left >= prevCenter) {
   2267                     distance1 = left - prevCenter;
   2268                 } else if (right <= prevCenter) {
   2269                     distance1 = prevCenter - right;
   2270                 }
   2271                 distance2 = prevTop - bottom;
   2272             } else if (prevLocation == FROM_LEFT) {
   2273                 if (bottom <= prevCenter) {
   2274                     distance1 = prevCenter - bottom;
   2275                 } else if (top >= prevCenter) {
   2276                     distance1 = top - prevCenter;
   2277                 }
   2278                 distance2 = left - prevRight;
   2279             } else if (prevLocation == FROM_RIGHT) {
   2280                 if (bottom <= prevCenter) {
   2281                     distance1 = prevCenter - bottom;
   2282                 } else if (top >= prevCenter) {
   2283                     distance1 = top - prevCenter;
   2284                 }
   2285                 distance2 = prevLeft - right;
   2286             }
   2287             if (distance1 < startEventDistance1
   2288                     || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
   2289                 startEvent = ev;
   2290                 startEventDistance1 = distance1;
   2291                 startEventDistance2 = distance2;
   2292             }
   2293 
   2294             // For each neighbor, figure out if it is above or below or left
   2295             // or right of me and compute the distance.
   2296             for (int jj = 0; jj < len; jj++) {
   2297                 if (jj == ii) {
   2298                     continue;
   2299                 }
   2300                 Event neighbor = mSelectedEvents.get(jj);
   2301                 int neighborLeft = (int) neighbor.left;
   2302                 int neighborRight = (int) neighbor.right;
   2303                 if (neighbor.endTime <= startTime) {
   2304                     // This neighbor is entirely above me.
   2305                     // If we overlap the same column, then compute the distance.
   2306                     if (neighborLeft < right && neighborRight > left) {
   2307                         int distance = startTime - neighbor.endTime;
   2308                         if (distance < upDistanceMin) {
   2309                             upDistanceMin = distance;
   2310                             upEvent = neighbor;
   2311                         } else if (distance == upDistanceMin) {
   2312                             int center = (left + right) / 2;
   2313                             int currentDistance = 0;
   2314                             int currentLeft = (int) upEvent.left;
   2315                             int currentRight = (int) upEvent.right;
   2316                             if (currentRight <= center) {
   2317                                 currentDistance = center - currentRight;
   2318                             } else if (currentLeft >= center) {
   2319                                 currentDistance = currentLeft - center;
   2320                             }
   2321 
   2322                             int neighborDistance = 0;
   2323                             if (neighborRight <= center) {
   2324                                 neighborDistance = center - neighborRight;
   2325                             } else if (neighborLeft >= center) {
   2326                                 neighborDistance = neighborLeft - center;
   2327                             }
   2328                             if (neighborDistance < currentDistance) {
   2329                                 upDistanceMin = distance;
   2330                                 upEvent = neighbor;
   2331                             }
   2332                         }
   2333                     }
   2334                 } else if (neighbor.startTime >= endTime) {
   2335                     // This neighbor is entirely below me.
   2336                     // If we overlap the same column, then compute the distance.
   2337                     if (neighborLeft < right && neighborRight > left) {
   2338                         int distance = neighbor.startTime - endTime;
   2339                         if (distance < downDistanceMin) {
   2340                             downDistanceMin = distance;
   2341                             downEvent = neighbor;
   2342                         } else if (distance == downDistanceMin) {
   2343                             int center = (left + right) / 2;
   2344                             int currentDistance = 0;
   2345                             int currentLeft = (int) downEvent.left;
   2346                             int currentRight = (int) downEvent.right;
   2347                             if (currentRight <= center) {
   2348                                 currentDistance = center - currentRight;
   2349                             } else if (currentLeft >= center) {
   2350                                 currentDistance = currentLeft - center;
   2351                             }
   2352 
   2353                             int neighborDistance = 0;
   2354                             if (neighborRight <= center) {
   2355                                 neighborDistance = center - neighborRight;
   2356                             } else if (neighborLeft >= center) {
   2357                                 neighborDistance = neighborLeft - center;
   2358                             }
   2359                             if (neighborDistance < currentDistance) {
   2360                                 downDistanceMin = distance;
   2361                                 downEvent = neighbor;
   2362                             }
   2363                         }
   2364                     }
   2365                 }
   2366 
   2367                 if (neighborLeft >= right) {
   2368                     // This neighbor is entirely to the right of me.
   2369                     // Take the closest neighbor in the y direction.
   2370                     int center = (top + bottom) / 2;
   2371                     int distance = 0;
   2372                     int neighborBottom = (int) neighbor.bottom;
   2373                     int neighborTop = (int) neighbor.top;
   2374                     if (neighborBottom <= center) {
   2375                         distance = center - neighborBottom;
   2376                     } else if (neighborTop >= center) {
   2377                         distance = neighborTop - center;
   2378                     }
   2379                     if (distance < rightDistanceMin) {
   2380                         rightDistanceMin = distance;
   2381                         rightEvent = neighbor;
   2382                     } else if (distance == rightDistanceMin) {
   2383                         // Pick the closest in the x direction
   2384                         int neighborDistance = neighborLeft - right;
   2385                         int currentDistance = (int) rightEvent.left - right;
   2386                         if (neighborDistance < currentDistance) {
   2387                             rightDistanceMin = distance;
   2388                             rightEvent = neighbor;
   2389                         }
   2390                     }
   2391                 } else if (neighborRight <= left) {
   2392                     // This neighbor is entirely to the left of me.
   2393                     // Take the closest neighbor in the y direction.
   2394                     int center = (top + bottom) / 2;
   2395                     int distance = 0;
   2396                     int neighborBottom = (int) neighbor.bottom;
   2397                     int neighborTop = (int) neighbor.top;
   2398                     if (neighborBottom <= center) {
   2399                         distance = center - neighborBottom;
   2400                     } else if (neighborTop >= center) {
   2401                         distance = neighborTop - center;
   2402                     }
   2403                     if (distance < leftDistanceMin) {
   2404                         leftDistanceMin = distance;
   2405                         leftEvent = neighbor;
   2406                     } else if (distance == leftDistanceMin) {
   2407                         // Pick the closest in the x direction
   2408                         int neighborDistance = left - neighborRight;
   2409                         int currentDistance = left - (int) leftEvent.right;
   2410                         if (neighborDistance < currentDistance) {
   2411                             leftDistanceMin = distance;
   2412                             leftEvent = neighbor;
   2413                         }
   2414                     }
   2415                 }
   2416             }
   2417             ev.nextUp = upEvent;
   2418             ev.nextDown = downEvent;
   2419             ev.nextLeft = leftEvent;
   2420             ev.nextRight = rightEvent;
   2421         }
   2422         mSelectedEvent = startEvent;
   2423     }
   2424 
   2425 
   2426     private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
   2427 
   2428         int color = event.color;
   2429 
   2430         // Fade visible boxes if event was declined.
   2431         boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
   2432         if (declined) {
   2433             int alpha = color & 0xff000000;
   2434             color &= 0x00ffffff;
   2435             int red = (color & 0x00ff0000) >> 16;
   2436             int green = (color & 0x0000ff00) >> 8;
   2437             int blue = (color & 0x0000ff);
   2438             color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
   2439             color += 0x7F7F7F + alpha;
   2440         }
   2441 
   2442         // If this event is selected, then use the selection color
   2443         if (mSelectedEvent == event) {
   2444             if (mSelectionMode == SELECTION_PRESSED || mSelectionMode == SELECTION_SELECTED) {
   2445                 // Also, remember the last selected event that we drew
   2446                 mPrevSelectedEvent = event;
   2447                 p.setColor(mSelectionColor);
   2448                 eventTextPaint.setColor(mSelectedEventTextColor);
   2449             } else if (mSelectionMode == SELECTION_LONGPRESS) {
   2450                 p.setColor(mSelectionColor);
   2451                 eventTextPaint.setColor(mSelectedEventTextColor);
   2452             } else {
   2453                 p.setColor(color);
   2454                 eventTextPaint.setColor(mEventTextColor);
   2455             }
   2456         } else {
   2457             p.setColor(color);
   2458             eventTextPaint.setColor(mEventTextColor);
   2459         }
   2460 
   2461 
   2462         RectF rf = mRectF;
   2463         rf.top = event.top;
   2464         rf.bottom = event.bottom;
   2465         rf.left = event.left;
   2466         rf.right = event.right - 1;
   2467 
   2468         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
   2469 
   2470         // Draw a darker border
   2471         float[] hsv = new float[3];
   2472         Color.colorToHSV(p.getColor(), hsv);
   2473         hsv[1] = 1.0f;
   2474         hsv[2] *= 0.75f;
   2475         mPaintBorder.setColor(Color.HSVToColor(hsv));
   2476         canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
   2477 
   2478         rf.left += 2;
   2479         rf.right -= 2;
   2480 
   2481         return rf;
   2482     }
   2483 
   2484     private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
   2485 
   2486     // Sanitize a string before passing it to drawText or else we get little
   2487     // squares. For newlines and tabs before a comma, delete the character.
   2488     // Otherwise, just replace them with a space.
   2489     private String drawTextSanitizer(String string) {
   2490         Matcher m = drawTextSanitizerFilter.matcher(string);
   2491         string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
   2492         return string;
   2493     }
   2494 
   2495     private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
   2496         if (!mDrawTextInEventRect) {
   2497             return;
   2498         }
   2499 
   2500         float width = rf.right - rf.left;
   2501         float height = rf.bottom - rf.top;
   2502 
   2503         // Leave one pixel extra space between lines
   2504         int lineHeight = mEventTextHeight + 1;
   2505 
   2506         // If the rectangle is too small for text, then return
   2507         if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
   2508             return;
   2509         }
   2510 
   2511         // Truncate the event title to a known (large enough) limit
   2512         String text = event.getTitleAndLocation();
   2513 
   2514         text = drawTextSanitizer(text);
   2515 
   2516         int len = text.length();
   2517         if (len > MAX_EVENT_TEXT_LEN) {
   2518             text = text.substring(0, MAX_EVENT_TEXT_LEN);
   2519             len = MAX_EVENT_TEXT_LEN;
   2520         }
   2521 
   2522         // Figure out how much space the event title will take, and create a
   2523         // String fragment that will fit in the rectangle.  Use multiple lines,
   2524         // if available.
   2525         p.getTextWidths(text, mCharWidths);
   2526         String fragment = text;
   2527         float top = rf.top + mEventTextAscent + topMargin;
   2528         int start = 0;
   2529 
   2530         // Leave one pixel extra space at the bottom
   2531         while (start < len && height >= (lineHeight + 1)) {
   2532             boolean lastLine = (height < 2 * lineHeight + 1);
   2533             // Skip leading spaces at the beginning of each line
   2534             do {
   2535                 char c = text.charAt(start);
   2536                 if (c != ' ') break;
   2537                 start += 1;
   2538             } while (start < len);
   2539 
   2540             float sum = 0;
   2541             int end = start;
   2542             for (int ii = start; ii < len; ii++) {
   2543                 char c = text.charAt(ii);
   2544 
   2545                 // If we found the end of a word, then remember the ending
   2546                 // position.
   2547                 if (c == ' ') {
   2548                     end = ii;
   2549                 }
   2550                 sum += mCharWidths[ii];
   2551                 // If adding this character would exceed the width and this
   2552                 // isn't the last line, then break the line at the previous
   2553                 // word.  If there was no previous word, then break this word.
   2554                 if (sum > width) {
   2555                     if (end > start && !lastLine) {
   2556                         // There was a previous word on this line.
   2557                         fragment = text.substring(start, end);
   2558                         start = end;
   2559                         break;
   2560                     }
   2561 
   2562                     // This is the only word and it is too long to fit on
   2563                     // the line (or this is the last line), so take as many
   2564                     // characters of this word as will fit.
   2565                     fragment = text.substring(start, ii);
   2566                     start = ii;
   2567                     break;
   2568                 }
   2569             }
   2570 
   2571             // If sum <= width, then we can fit the rest of the text on
   2572             // this line.
   2573             if (sum <= width) {
   2574                 fragment = text.substring(start, len);
   2575                 start = len;
   2576             }
   2577 
   2578             canvas.drawText(fragment, rf.left + 1, top, p);
   2579 
   2580             top += lineHeight;
   2581             height -= lineHeight;
   2582         }
   2583     }
   2584 
   2585     private void updateEventDetails() {
   2586         if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
   2587                 || mSelectionMode == SELECTION_LONGPRESS) {
   2588             mPopup.dismiss();
   2589             return;
   2590         }
   2591         if (mLastPopupEventID == mSelectedEvent.id) {
   2592             return;
   2593         }
   2594 
   2595         mLastPopupEventID = mSelectedEvent.id;
   2596 
   2597         // Remove any outstanding callbacks to dismiss the popup.
   2598         getHandler().removeCallbacks(mDismissPopup);
   2599 
   2600         Event event = mSelectedEvent;
   2601         TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
   2602         titleView.setText(event.title);
   2603 
   2604         ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
   2605         imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
   2606 
   2607         imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
   2608         imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
   2609 
   2610         int flags;
   2611         if (event.allDay) {
   2612             flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
   2613                     DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
   2614         } else {
   2615             flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
   2616                     | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
   2617                     | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   2618         }
   2619         if (DateFormat.is24HourFormat(mParentActivity)) {
   2620             flags |= DateUtils.FORMAT_24HOUR;
   2621         }
   2622         String timeRange = Utils.formatDateRange(mParentActivity,
   2623                 event.startMillis, event.endMillis, flags);
   2624         TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
   2625         timeView.setText(timeRange);
   2626 
   2627         TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
   2628         final boolean empty = TextUtils.isEmpty(event.location);
   2629         whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
   2630         if (!empty) whereView.setText(event.location);
   2631 
   2632         mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
   2633         postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
   2634 
   2635         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
   2636     }
   2637 
   2638     // The following routines are called from the parent activity when certain
   2639     // touch events occur.
   2640 
   2641     void doDown(MotionEvent ev) {
   2642         mTouchMode = TOUCH_MODE_DOWN;
   2643         mViewStartX = 0;
   2644         mOnFlingCalled = false;
   2645         getHandler().removeCallbacks(mContinueScroll);
   2646     }
   2647 
   2648     void doSingleTapUp(MotionEvent ev) {
   2649         int x = (int) ev.getX();
   2650         int y = (int) ev.getY();
   2651         int selectedDay = mSelectionDay;
   2652         int selectedHour = mSelectionHour;
   2653 
   2654         boolean validPosition = setSelectionFromPosition(x, y);
   2655         if (!validPosition) {
   2656             // return if the touch wasn't on an area of concern
   2657             return;
   2658         }
   2659 
   2660         mSelectionMode = SELECTION_SELECTED;
   2661         mRedrawScreen = true;
   2662         invalidate();
   2663 
   2664         boolean launchNewView = false;
   2665         if (mSelectedEvent != null) {
   2666             // If the tap is on an event, launch the "View event" view
   2667             launchNewView = true;
   2668         } else if (mSelectedEvent == null && selectedDay == mSelectionDay
   2669                 && selectedHour == mSelectionHour) {
   2670             // If the tap is on an already selected hour slot,
   2671             // then launch the Day/Agenda view. Otherwise, just select the hour
   2672             // slot.
   2673             launchNewView = true;
   2674         }
   2675 
   2676         if (launchNewView) {
   2677             switchViews(false /* not the trackball */);
   2678         }
   2679     }
   2680 
   2681     void doLongPress(MotionEvent ev) {
   2682         int x = (int) ev.getX();
   2683         int y = (int) ev.getY();
   2684 
   2685         boolean validPosition = setSelectionFromPosition(x, y);
   2686         if (!validPosition) {
   2687             // return if the touch wasn't on an area of concern
   2688             return;
   2689         }
   2690 
   2691         mSelectionMode = SELECTION_LONGPRESS;
   2692         mRedrawScreen = true;
   2693         invalidate();
   2694         performLongClick();
   2695     }
   2696 
   2697     void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
   2698         // Use the distance from the current point to the initial touch instead
   2699         // of deltaX and deltaY to avoid accumulating floating-point rounding
   2700         // errors.  Also, we don't need floats, we can use ints.
   2701         int distanceX = (int) e1.getX() - (int) e2.getX();
   2702         int distanceY = (int) e1.getY() - (int) e2.getY();
   2703 
   2704         // If we haven't figured out the predominant scroll direction yet,
   2705         // then do it now.
   2706         if (mTouchMode == TOUCH_MODE_DOWN) {
   2707             int absDistanceX = Math.abs(distanceX);
   2708             int absDistanceY = Math.abs(distanceY);
   2709             mScrollStartY = mViewStartY;
   2710             mPreviousDistanceX = 0;
   2711             mPreviousDirection = 0;
   2712 
   2713             // If the x distance is at least twice the y distance, then lock
   2714             // the scroll horizontally.  Otherwise scroll vertically.
   2715             if (absDistanceX >= 2 * absDistanceY) {
   2716                 mTouchMode = TOUCH_MODE_HSCROLL;
   2717                 mViewStartX = distanceX;
   2718                 initNextView(-mViewStartX);
   2719             } else {
   2720                 mTouchMode = TOUCH_MODE_VSCROLL;
   2721             }
   2722         } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
   2723             // We are already scrolling horizontally, so check if we
   2724             // changed the direction of scrolling so that the other week
   2725             // is now visible.
   2726             mViewStartX = distanceX;
   2727             if (distanceX != 0) {
   2728                 int direction = (distanceX > 0) ? 1 : -1;
   2729                 if (direction != mPreviousDirection) {
   2730                     // The user has switched the direction of scrolling
   2731                     // so re-init the next view
   2732                     initNextView(-mViewStartX);
   2733                     mPreviousDirection = direction;
   2734                 }
   2735             }
   2736 
   2737             // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
   2738             // then change the title to the new day (or week), but only
   2739             // if we haven't already changed the title.
   2740             if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
   2741                 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
   2742                     CalendarView view = mParentActivity.getNextView();
   2743                     mTitleTextView.setText(view.mDateRange);
   2744                 }
   2745             } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
   2746                 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
   2747                     CalendarView view = mParentActivity.getNextView();
   2748                     mTitleTextView.setText(view.mDateRange);
   2749                 }
   2750             } else {
   2751                 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
   2752                         || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
   2753                     mTitleTextView.setText(mDateRange);
   2754                 }
   2755             }
   2756             mPreviousDistanceX = distanceX;
   2757         }
   2758 
   2759         if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
   2760             mViewStartY = mScrollStartY + distanceY;
   2761             if (mViewStartY < 0) {
   2762                 mViewStartY = 0;
   2763             } else if (mViewStartY > mMaxViewStartY) {
   2764                 mViewStartY = mMaxViewStartY;
   2765             }
   2766             computeFirstHour();
   2767         }
   2768 
   2769         mScrolling = true;
   2770 
   2771         if (mSelectionMode != SELECTION_HIDDEN) {
   2772             mSelectionMode = SELECTION_HIDDEN;
   2773             mRedrawScreen = true;
   2774         }
   2775         invalidate();
   2776     }
   2777 
   2778     void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   2779         mTouchMode = TOUCH_MODE_INITIAL_STATE;
   2780         mSelectionMode = SELECTION_HIDDEN;
   2781         mOnFlingCalled = true;
   2782         int deltaX = (int) e2.getX() - (int) e1.getX();
   2783         int distanceX = Math.abs(deltaX);
   2784         int deltaY = (int) e2.getY() - (int) e1.getY();
   2785         int distanceY = Math.abs(deltaY);
   2786 
   2787         if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
   2788             boolean switchForward = initNextView(deltaX);
   2789             CalendarView view = mParentActivity.getNextView();
   2790             mTitleTextView.setText(view.mDateRange);
   2791             mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
   2792             mViewStartX = 0;
   2793             return;
   2794         }
   2795 
   2796         // Continue scrolling vertically
   2797         mContinueScroll.init((int) velocityY / 20);
   2798         post(mContinueScroll);
   2799     }
   2800 
   2801     private boolean initNextView(int deltaX) {
   2802         // Change the view to the previous day or week
   2803         CalendarView view = mParentActivity.getNextView();
   2804         Time date = view.mBaseDate;
   2805         date.set(mBaseDate);
   2806         boolean switchForward;
   2807         if (deltaX > 0) {
   2808             date.monthDay -= mNumDays;
   2809             view.mSelectionDay = mSelectionDay - mNumDays;
   2810             switchForward = false;
   2811         } else {
   2812             date.monthDay += mNumDays;
   2813             view.mSelectionDay = mSelectionDay + mNumDays;
   2814             switchForward = true;
   2815         }
   2816         date.normalize(true /* ignore isDst */);
   2817         initView(view);
   2818         view.layout(getLeft(), getTop(), getRight(), getBottom());
   2819         view.reloadEvents();
   2820         return switchForward;
   2821     }
   2822 
   2823     @Override
   2824     public boolean onTouchEvent(MotionEvent ev) {
   2825         int action = ev.getAction();
   2826 
   2827         switch (action) {
   2828         case MotionEvent.ACTION_DOWN:
   2829             mParentActivity.mGestureDetector.onTouchEvent(ev);
   2830             return true;
   2831 
   2832         case MotionEvent.ACTION_MOVE:
   2833             mParentActivity.mGestureDetector.onTouchEvent(ev);
   2834             return true;
   2835 
   2836         case MotionEvent.ACTION_UP:
   2837             mParentActivity.mGestureDetector.onTouchEvent(ev);
   2838             if (mOnFlingCalled) {
   2839                 return true;
   2840             }
   2841             if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
   2842                 mTouchMode = TOUCH_MODE_INITIAL_STATE;
   2843                 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
   2844                     // The user has gone beyond the threshold so switch views
   2845                     mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
   2846                     mViewStartX = 0;
   2847                     return true;
   2848                 } else {
   2849                     // Not beyond the threshold so invalidate which will cause
   2850                     // the view to snap back.  Also call recalc() to ensure
   2851                     // that we have the correct starting date and title.
   2852                     recalc();
   2853                     mTitleTextView.setText(mDateRange);
   2854                     invalidate();
   2855                     mViewStartX = 0;
   2856                 }
   2857             }
   2858 
   2859             // If we were scrolling, then reset the selected hour so that it
   2860             // is visible.
   2861             if (mScrolling) {
   2862                 mScrolling = false;
   2863                 resetSelectedHour();
   2864                 mRedrawScreen = true;
   2865                 invalidate();
   2866             }
   2867             return true;
   2868 
   2869         // This case isn't expected to happen.
   2870         case MotionEvent.ACTION_CANCEL:
   2871             mParentActivity.mGestureDetector.onTouchEvent(ev);
   2872             mScrolling = false;
   2873             resetSelectedHour();
   2874             return true;
   2875 
   2876         default:
   2877             if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
   2878                 return true;
   2879             }
   2880             return super.onTouchEvent(ev);
   2881         }
   2882     }
   2883 
   2884     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
   2885         MenuItem item;
   2886 
   2887         // If the trackball is held down, then the context menu pops up and
   2888         // we never get onKeyUp() for the long-press.  So check for it here
   2889         // and change the selection to the long-press state.
   2890         if (mSelectionMode != SELECTION_LONGPRESS) {
   2891             mSelectionMode = SELECTION_LONGPRESS;
   2892             mRedrawScreen = true;
   2893             invalidate();
   2894         }
   2895 
   2896         final long startMillis = getSelectedTimeInMillis();
   2897         int flags = DateUtils.FORMAT_SHOW_TIME
   2898                 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
   2899                 | DateUtils.FORMAT_SHOW_WEEKDAY;
   2900         final String title = Utils.formatDateRange(mParentActivity, startMillis, startMillis,
   2901                 flags);
   2902         menu.setHeaderTitle(title);
   2903 
   2904         int numSelectedEvents = mSelectedEvents.size();
   2905         if (mNumDays == 1) {
   2906             // Day view.
   2907 
   2908             // If there is a selected event, then allow it to be viewed and
   2909             // edited.
   2910             if (numSelectedEvents >= 1) {
   2911                 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
   2912                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2913                 item.setIcon(android.R.drawable.ic_menu_info_details);
   2914 
   2915                 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
   2916                 if (accessLevel == ACCESS_LEVEL_EDIT) {
   2917                     item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
   2918                     item.setOnMenuItemClickListener(mContextMenuHandler);
   2919                     item.setIcon(android.R.drawable.ic_menu_edit);
   2920                     item.setAlphabeticShortcut('e');
   2921                 }
   2922 
   2923                 if (accessLevel >= ACCESS_LEVEL_DELETE) {
   2924                     item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
   2925                     item.setOnMenuItemClickListener(mContextMenuHandler);
   2926                     item.setIcon(android.R.drawable.ic_menu_delete);
   2927                 }
   2928 
   2929                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
   2930                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2931                 item.setIcon(android.R.drawable.ic_menu_add);
   2932                 item.setAlphabeticShortcut('n');
   2933             } else {
   2934                 // Otherwise, if the user long-pressed on a blank hour, allow
   2935                 // them to create an event.  They can also do this by tapping.
   2936                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
   2937                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2938                 item.setIcon(android.R.drawable.ic_menu_add);
   2939                 item.setAlphabeticShortcut('n');
   2940             }
   2941         } else {
   2942             // Week view.
   2943 
   2944             // If there is a selected event, then allow it to be viewed and
   2945             // edited.
   2946             if (numSelectedEvents >= 1) {
   2947                 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
   2948                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2949                 item.setIcon(android.R.drawable.ic_menu_info_details);
   2950 
   2951                 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
   2952                 if (accessLevel == ACCESS_LEVEL_EDIT) {
   2953                     item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
   2954                     item.setOnMenuItemClickListener(mContextMenuHandler);
   2955                     item.setIcon(android.R.drawable.ic_menu_edit);
   2956                     item.setAlphabeticShortcut('e');
   2957                 }
   2958 
   2959                 if (accessLevel >= ACCESS_LEVEL_DELETE) {
   2960                     item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
   2961                     item.setOnMenuItemClickListener(mContextMenuHandler);
   2962                     item.setIcon(android.R.drawable.ic_menu_delete);
   2963                 }
   2964 
   2965                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
   2966                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2967                 item.setIcon(android.R.drawable.ic_menu_add);
   2968                 item.setAlphabeticShortcut('n');
   2969 
   2970                 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
   2971                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2972                 item.setIcon(android.R.drawable.ic_menu_day);
   2973                 item.setAlphabeticShortcut('d');
   2974 
   2975                 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
   2976                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2977                 item.setIcon(android.R.drawable.ic_menu_agenda);
   2978                 item.setAlphabeticShortcut('a');
   2979             } else {
   2980                 // No events are selected
   2981                 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
   2982                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2983                 item.setIcon(android.R.drawable.ic_menu_add);
   2984                 item.setAlphabeticShortcut('n');
   2985 
   2986                 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
   2987                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2988                 item.setIcon(android.R.drawable.ic_menu_day);
   2989                 item.setAlphabeticShortcut('d');
   2990 
   2991                 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
   2992                 item.setOnMenuItemClickListener(mContextMenuHandler);
   2993                 item.setIcon(android.R.drawable.ic_menu_agenda);
   2994                 item.setAlphabeticShortcut('a');
   2995             }
   2996         }
   2997 
   2998         mPopup.dismiss();
   2999     }
   3000 
   3001     private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
   3002         public boolean onMenuItemClick(MenuItem item) {
   3003             switch (item.getItemId()) {
   3004                 case MenuHelper.MENU_EVENT_VIEW: {
   3005                     if (mSelectedEvent != null) {
   3006                         long id = mSelectedEvent.id;
   3007                         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
   3008                         Intent intent = new Intent(Intent.ACTION_VIEW);
   3009                         intent.setData(eventUri);
   3010                         intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
   3011                         intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
   3012                         intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
   3013                         mParentActivity.startActivity(intent);
   3014                     }
   3015                     break;
   3016                 }
   3017                 case MenuHelper.MENU_EVENT_EDIT: {
   3018                     if (mSelectedEvent != null) {
   3019                         long id = mSelectedEvent.id;
   3020                         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
   3021                         Intent intent = new Intent(Intent.ACTION_EDIT);
   3022                         intent.setData(eventUri);
   3023                         intent.setClassName(mParentActivity, EditEvent.class.getName());
   3024                         intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
   3025                         intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
   3026                         mParentActivity.startActivity(intent);
   3027                     }
   3028                     break;
   3029                 }
   3030                 case MenuHelper.MENU_DAY: {
   3031                     long startMillis = getSelectedTimeInMillis();
   3032                     Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
   3033                     break;
   3034                 }
   3035                 case MenuHelper.MENU_AGENDA: {
   3036                     long startMillis = getSelectedTimeInMillis();
   3037                     Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
   3038                     break;
   3039                 }
   3040                 case MenuHelper.MENU_EVENT_CREATE: {
   3041                     long startMillis = getSelectedTimeInMillis();
   3042                     long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
   3043                     Intent intent = new Intent(Intent.ACTION_VIEW);
   3044                     intent.setClassName(mParentActivity, EditEvent.class.getName());
   3045                     intent.putExtra(EVENT_BEGIN_TIME, startMillis);
   3046                     intent.putExtra(EVENT_END_TIME, endMillis);
   3047                     intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
   3048                     mParentActivity.startActivity(intent);
   3049                     break;
   3050                 }
   3051                 case MenuHelper.MENU_EVENT_DELETE: {
   3052                     if (mSelectedEvent != null) {
   3053                         Event selectedEvent = mSelectedEvent;
   3054                         long begin = selectedEvent.startMillis;
   3055                         long end = selectedEvent.endMillis;
   3056                         long id = selectedEvent.id;
   3057                         mDeleteEventHelper.delete(begin, end, id, -1);
   3058                     }
   3059                     break;
   3060                 }
   3061                 default: {
   3062                     return false;
   3063                 }
   3064             }
   3065             return true;
   3066         }
   3067     }
   3068 
   3069     private static int getEventAccessLevel(Context context, Event e) {
   3070         ContentResolver cr = context.getContentResolver();
   3071 
   3072         int visibility = Calendars.NO_ACCESS;
   3073         int relationship = Attendees.RELATIONSHIP_ORGANIZER;
   3074 
   3075         // Get the calendar id for this event
   3076         Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
   3077                 new String[] { Events.CALENDAR_ID },
   3078                 null /* selection */,
   3079                 null /* selectionArgs */,
   3080                 null /* sort */);
   3081 
   3082         if (cursor == null) {
   3083             return ACCESS_LEVEL_NONE;
   3084         }
   3085 
   3086         if (cursor.getCount() == 0) {
   3087             cursor.close();
   3088             return ACCESS_LEVEL_NONE;
   3089         }
   3090 
   3091         cursor.moveToFirst();
   3092         long calId = cursor.getLong(0);
   3093         cursor.close();
   3094 
   3095         Uri uri = Calendars.CONTENT_URI;
   3096         String where = String.format(CALENDARS_WHERE, calId);
   3097         cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
   3098 
   3099         String calendarOwnerAccount = null;
   3100         if (cursor != null) {
   3101             cursor.moveToFirst();
   3102             visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
   3103             calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
   3104             cursor.close();
   3105         }
   3106 
   3107         if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
   3108             return ACCESS_LEVEL_NONE;
   3109         }
   3110 
   3111         if (e.guestsCanModify) {
   3112             return ACCESS_LEVEL_EDIT;
   3113         }
   3114 
   3115         if (!TextUtils.isEmpty(calendarOwnerAccount) &&
   3116                 calendarOwnerAccount.equalsIgnoreCase(e.organizer)) {
   3117             return ACCESS_LEVEL_EDIT;
   3118         }
   3119 
   3120         return ACCESS_LEVEL_DELETE;
   3121     }
   3122 
   3123     /**
   3124      * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
   3125      * If the touch position is not within the displayed grid, then this
   3126      * method returns false.
   3127      *
   3128      * @param x the x position of the touch
   3129      * @param y the y position of the touch
   3130      * @return true if the touch position is valid
   3131      */
   3132     private boolean setSelectionFromPosition(int x, int y) {
   3133         if (x < mHoursWidth) {
   3134             return false;
   3135         }
   3136 
   3137         int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
   3138         if (day >= mNumDays) {
   3139             day = mNumDays - 1;
   3140         }
   3141         day += mFirstJulianDay;
   3142         int hour;
   3143         if (y < mFirstCell + mFirstHourOffset) {
   3144             mSelectionAllDay = true;
   3145         } else {
   3146             hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
   3147             hour += mFirstHour;
   3148             mSelectionHour = hour;
   3149             mSelectionAllDay = false;
   3150         }
   3151         mSelectionDay = day;
   3152         findSelectedEvent(x, y);
   3153 //        Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
   3154 //                + " hour: " + hour
   3155 //                + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
   3156 //        if (mSelectedEvent != null) {
   3157 //            Log.i("Cal", "  num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
   3158 //            for (Event ev : mSelectedEvents) {
   3159 //                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
   3160 //                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
   3161 //                String timeRange = Utils.formatDateRange(mParentActivity,
   3162 //                        ev.startMillis, ev.endMillis, flags);
   3163 //
   3164 //                Log.i("Cal", "  " + timeRange + " " + ev.title);
   3165 //            }
   3166 //        }
   3167         return true;
   3168     }
   3169 
   3170     private void findSelectedEvent(int x, int y) {
   3171         int date = mSelectionDay;
   3172         int cellWidth = mCellWidth;
   3173         ArrayList<Event> events = mEvents;
   3174         int numEvents = events.size();
   3175         int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
   3176         int top = 0;
   3177         mSelectedEvent = null;
   3178 
   3179         mSelectedEvents.clear();
   3180         if (mSelectionAllDay) {
   3181             float yDistance;
   3182             float minYdistance = 10000.0f;  // any large number
   3183             Event closestEvent = null;
   3184             float drawHeight = mAllDayHeight;
   3185             int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
   3186             for (int i = 0; i < numEvents; i++) {
   3187                 Event event = events.get(i);
   3188                 if (!event.allDay) {
   3189                     continue;
   3190                 }
   3191 
   3192                 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
   3193                     float numRectangles = event.getMaxColumns();
   3194                     float height = drawHeight / numRectangles;
   3195                     if (height > MAX_ALLDAY_EVENT_HEIGHT) {
   3196                         height = MAX_ALLDAY_EVENT_HEIGHT;
   3197                     }
   3198                     float eventTop = yOffset + height * event.getColumn();
   3199                     float eventBottom = eventTop + height;
   3200                     if (eventTop < y && eventBottom > y) {
   3201                         // If the touch is inside the event rectangle, then
   3202                         // add the event.
   3203                         mSelectedEvents.add(event);
   3204                         closestEvent = event;
   3205                         break;
   3206                     } else {
   3207                         // Find the closest event
   3208                         if (eventTop >= y) {
   3209                             yDistance = eventTop - y;
   3210                         } else {
   3211                             yDistance = y - eventBottom;
   3212                         }
   3213                         if (yDistance < minYdistance) {
   3214                             minYdistance = yDistance;
   3215                             closestEvent = event;
   3216                         }
   3217                     }
   3218                 }
   3219             }
   3220             mSelectedEvent = closestEvent;
   3221             return;
   3222         }
   3223 
   3224         // Adjust y for the scrollable bitmap
   3225         y += mViewStartY - mFirstCell;
   3226 
   3227         // Use a region around (x,y) for the selection region
   3228         Rect region = mRect;
   3229         region.left = x - 10;
   3230         region.right = x + 10;
   3231         region.top = y - 10;
   3232         region.bottom = y + 10;
   3233 
   3234         EventGeometry geometry = mEventGeometry;
   3235 
   3236         for (int i = 0; i < numEvents; i++) {
   3237             Event event = events.get(i);
   3238             // Compute the event rectangle.
   3239             if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
   3240                 continue;
   3241             }
   3242 
   3243             // If the event intersects the selection region, then add it to
   3244             // mSelectedEvents.
   3245             if (geometry.eventIntersectsSelection(event, region)) {
   3246                 mSelectedEvents.add(event);
   3247             }
   3248         }
   3249 
   3250         // If there are any events in the selected region, then assign the
   3251         // closest one to mSelectedEvent.
   3252         if (mSelectedEvents.size() > 0) {
   3253             int len = mSelectedEvents.size();
   3254             Event closestEvent = null;
   3255             float minDist = mViewWidth + mViewHeight;  // some large distance
   3256             for (int index = 0; index < len; index++) {
   3257                 Event ev = mSelectedEvents.get(index);
   3258                 float dist = geometry.pointToEvent(x, y, ev);
   3259                 if (dist < minDist) {
   3260                     minDist = dist;
   3261                     closestEvent = ev;
   3262                 }
   3263             }
   3264             mSelectedEvent = closestEvent;
   3265 
   3266             // Keep the selected hour and day consistent with the selected
   3267             // event.  They could be different if we touched on an empty hour
   3268             // slot very close to an event in the previous hour slot.  In
   3269             // that case we will select the nearby event.
   3270             int startDay = mSelectedEvent.startDay;
   3271             int endDay = mSelectedEvent.endDay;
   3272             if (mSelectionDay < startDay) {
   3273                 mSelectionDay = startDay;
   3274             } else if (mSelectionDay > endDay) {
   3275                 mSelectionDay = endDay;
   3276             }
   3277 
   3278             int startHour = mSelectedEvent.startTime / 60;
   3279             int endHour;
   3280             if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
   3281                 endHour = (mSelectedEvent.endTime - 1) / 60;
   3282             } else {
   3283                 endHour = mSelectedEvent.endTime / 60;
   3284             }
   3285 
   3286             if (mSelectionHour < startHour) {
   3287                 mSelectionHour = startHour;
   3288             } else if (mSelectionHour > endHour) {
   3289                 mSelectionHour = endHour;
   3290             }
   3291         }
   3292     }
   3293 
   3294     // Encapsulates the code to continue the scrolling after the
   3295     // finger is lifted.  Instead of stopping the scroll immediately,
   3296     // the scroll continues to "free spin" and gradually slows down.
   3297     private class ContinueScroll implements Runnable {
   3298         int mSignDeltaY;
   3299         int mAbsDeltaY;
   3300         float mFloatDeltaY;
   3301         long mFreeSpinTime;
   3302         private static final float FRICTION_COEF = 0.7F;
   3303         private static final long FREE_SPIN_MILLIS = 180;
   3304         private static final int MAX_DELTA = 60;
   3305         private static final int SCROLL_REPEAT_INTERVAL = 30;
   3306 
   3307         public void init(int deltaY) {
   3308             mSignDeltaY = 0;
   3309             if (deltaY > 0) {
   3310                 mSignDeltaY = 1;
   3311             } else if (deltaY < 0) {
   3312                 mSignDeltaY = -1;
   3313             }
   3314             mAbsDeltaY = Math.abs(deltaY);
   3315 
   3316             // Limit the maximum speed
   3317             if (mAbsDeltaY > MAX_DELTA) {
   3318                 mAbsDeltaY = MAX_DELTA;
   3319             }
   3320             mFloatDeltaY = mAbsDeltaY;
   3321             mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
   3322 //            Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
   3323 //                    + " mViewStartY: " + mViewStartY);
   3324         }
   3325 
   3326         public void run() {
   3327             long time = System.currentTimeMillis();
   3328 
   3329             // Start out with a frictionless "free spin"
   3330             if (time > mFreeSpinTime) {
   3331                 // If the delta is small, then apply a fixed deceleration.
   3332                 // Otherwise
   3333                 if (mAbsDeltaY <= 10) {
   3334                     mAbsDeltaY -= 2;
   3335                 } else {
   3336                     mFloatDeltaY *= FRICTION_COEF;
   3337                     mAbsDeltaY = (int) mFloatDeltaY;
   3338                 }
   3339 
   3340                 if (mAbsDeltaY < 0) {
   3341                     mAbsDeltaY = 0;
   3342                 }
   3343             }
   3344 
   3345             if (mSignDeltaY == 1) {
   3346                 mViewStartY -= mAbsDeltaY;
   3347             } else {
   3348                 mViewStartY += mAbsDeltaY;
   3349             }
   3350 //            Log.i("Cal", "  scroll: mAbsDeltaY: " + mAbsDeltaY
   3351 //                    + " mViewStartY: " + mViewStartY);
   3352 
   3353             if (mViewStartY < 0) {
   3354                 mViewStartY = 0;
   3355                 mAbsDeltaY = 0;
   3356             } else if (mViewStartY > mMaxViewStartY) {
   3357                 mViewStartY = mMaxViewStartY;
   3358                 mAbsDeltaY = 0;
   3359             }
   3360 
   3361             computeFirstHour();
   3362 
   3363             if (mAbsDeltaY > 0) {
   3364                 postDelayed(this, SCROLL_REPEAT_INTERVAL);
   3365             } else {
   3366                 // Done scrolling.
   3367                 mScrolling = false;
   3368                 resetSelectedHour();
   3369                 mRedrawScreen = true;
   3370             }
   3371 
   3372             invalidate();
   3373         }
   3374     }
   3375 
   3376     /**
   3377      * Cleanup the pop-up and timers.
   3378      */
   3379     public void cleanup() {
   3380         // Protect against null-pointer exceptions
   3381         if (mPopup != null) {
   3382             mPopup.dismiss();
   3383         }
   3384         mLastPopupEventID = INVALID_EVENT_ID;
   3385         Handler handler = getHandler();
   3386         if (handler != null) {
   3387             handler.removeCallbacks(mDismissPopup);
   3388             handler.removeCallbacks(mUpdateCurrentTime);
   3389         }
   3390 
   3391         // Turn off redraw
   3392         mRemeasure = false;
   3393         mRedrawScreen = false;
   3394 
   3395         // clear the cached values for accessibility support
   3396         mPrevSelectionDay = 0;
   3397         mPrevSelectionHour = 0;
   3398         mPrevTitleTextViewText = null;
   3399     }
   3400 
   3401     /**
   3402      * Restart the update timer
   3403      */
   3404     public void updateView() {
   3405         mUpdateTZ.run();
   3406         post(mUpdateCurrentTime);
   3407     }
   3408 
   3409     @Override protected void onDetachedFromWindow() {
   3410         cleanup();
   3411         if (mBitmap != null) {
   3412             mBitmap.recycle();
   3413             mBitmap = null;
   3414         }
   3415         super.onDetachedFromWindow();
   3416     }
   3417 
   3418     class DismissPopup implements Runnable {
   3419         public void run() {
   3420             // Protect against null-pointer exceptions
   3421             if (mPopup != null) {
   3422                 mPopup.dismiss();
   3423             }
   3424         }
   3425     }
   3426 
   3427     class UpdateCurrentTime implements Runnable {
   3428         public void run() {
   3429             long currentTime = System.currentTimeMillis();
   3430             mCurrentTime.set(currentTime);
   3431             //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
   3432             postDelayed(mUpdateCurrentTime,
   3433                     UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
   3434             mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
   3435             mRedrawScreen = true;
   3436             invalidate();
   3437         }
   3438     }
   3439 }
   3440