revive a hack that creates a local calendar if the google account authenticator is...
[android/platform/packages/apps/Calendar.git] / src / com / android / calendar / MonthView.java
1 /*
2  * Copyright (C) 2006 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.Context;
23 import android.content.Intent;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Paint;
30 import android.graphics.PorterDuff;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.os.Handler;
34 import android.os.SystemClock;
35 import android.provider.Calendar.BusyBits;
36 import android.text.format.DateFormat;
37 import android.text.format.DateUtils;
38 import android.text.format.Time;
39 import android.util.DayOfMonthCursor;
40 import android.util.Log;
41 import android.util.SparseArray;
42 import android.view.ContextMenu;
43 import android.view.GestureDetector;
44 import android.view.Gravity;
45 import android.view.KeyEvent;
46 import android.view.LayoutInflater;
47 import android.view.MenuItem;
48 import android.view.MotionEvent;
49 import android.view.View;
50 import android.view.ViewConfiguration;
51 import android.view.ContextMenu.ContextMenuInfo;
52 import android.widget.PopupWindow;
53 import android.widget.TextView;
54
55 import java.util.ArrayList;
56 import java.util.Calendar;
57
58 public class MonthView extends View implements View.OnCreateContextMenuListener {
59
60     private static final boolean PROFILE_LOAD_TIME = false;
61     private static final boolean DEBUG_BUSYBITS = false;
62
63     private static float mScale = 0; // Used for supporting different screen densities
64     private static int WEEK_GAP = 0;
65     private static int MONTH_DAY_GAP = 1;
66     private static float HOUR_GAP = 0.5f;
67
68     private static int MONTH_DAY_TEXT_SIZE = 20;
69     private static int WEEK_BANNER_HEIGHT = 17;
70     private static int WEEK_TEXT_SIZE = 15;
71     private static int WEEK_TEXT_PADDING = 3;
72     private static int BUSYBIT_WIDTH = 10;
73     private static int BUSYBIT_RIGHT_MARGIN = 3;
74     private static int BUSYBIT_TOP_BOTTOM_MARGIN = 7;
75
76     private static int HORIZONTAL_FLING_THRESHOLD = 50;
77
78     private int mCellHeight;
79     private int mBorder;
80     private boolean mLaunchDayView;
81
82     private GestureDetector mGestureDetector;
83
84     private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
85
86     private Time mToday;
87     private Time mViewCalendar;
88     private Time mSavedTime = new Time();   // the time when we entered this view
89
90     // This Time object is used to set the time for the other Month view.
91     private Time mOtherViewCalendar = new Time();
92
93     // This Time object is used for temporary calculations and is allocated
94     // once to avoid extra garbage collection
95     private Time mTempTime = new Time();
96
97     private DayOfMonthCursor mCursor;
98
99     private Drawable mBoxSelected;
100     private Drawable mBoxPressed;
101     private Drawable mBoxLongPressed;
102     private Drawable mDnaEmpty;
103     private Drawable mDnaTop;
104     private Drawable mDnaMiddle;
105     private Drawable mDnaBottom;
106     private int mCellWidth;
107
108     private Resources mResources;
109     private MonthActivity mParentActivity;
110     private Navigator mNavigator;
111     private final EventGeometry mEventGeometry;
112
113     // Pre-allocate and reuse
114     private Rect mRect = new Rect();
115
116     // The number of hours represented by one busy bit
117     private static final int HOURS_PER_BUSY_SLOT = 4;
118
119     // The number of database intervals represented by one busy bit (slot)
120     private static final int INTERVALS_PER_BUSY_SLOT = 4 * 60 / BusyBits.MINUTES_PER_BUSY_INTERVAL;
121
122     // The bit mask for coalescing the raw busy bits from the database
123     // (1 bit per hour) into the busy bits per slot (4-hour slots).
124     private static final int BUSY_SLOT_MASK = (1 << INTERVALS_PER_BUSY_SLOT) - 1;
125
126     // The number of slots in a day
127     private static final int SLOTS_PER_DAY = 24 / HOURS_PER_BUSY_SLOT;
128
129     // There is one "busy" bit for each slot of time.
130     private byte[][] mBusyBits = new byte[31][SLOTS_PER_DAY];
131
132     // Raw busy bits from database
133     private int[] mRawBusyBits = new int[31];
134     private int[] mAllDayCounts = new int[31];
135
136     private PopupWindow mPopup;
137     private View mPopupView;
138     private static final int POPUP_HEIGHT = 100;
139     private int mPreviousPopupHeight;
140     private static final int POPUP_DISMISS_DELAY = 3000;
141     private DismissPopup mDismissPopup = new DismissPopup();
142
143     // For drawing to an off-screen Canvas
144     private Bitmap mBitmap;
145     private Canvas mCanvas;
146     private boolean mRedrawScreen = true;
147     private Rect mBitmapRect = new Rect();
148     private boolean mAnimating;
149
150     // These booleans disable features that were taken out of the spec.
151     private boolean mShowWeekNumbers = false;
152     private boolean mShowToast = false;
153
154     // Bitmap caches.
155     // These improve performance by minimizing calls to NinePatchDrawable.draw() for common
156     // drawables for events and day backgrounds.
157     // mEventBitmapCache is indexed by an integer constructed from the bits in the busyBits
158     // field. It is not expected to be larger than 12 bits (if so, we should switch to using a Map).
159     // mDayBitmapCache is indexed by a unique integer constructed from the width/height.
160     private SparseArray<Bitmap> mEventBitmapCache = new SparseArray<Bitmap>(1<<SLOTS_PER_DAY);
161     private SparseArray<Bitmap> mDayBitmapCache = new SparseArray<Bitmap>(4);
162
163     private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
164
165     /**
166      * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
167      */
168     private static final int SELECTION_HIDDEN = 0;
169     private static final int SELECTION_PRESSED = 1;
170     private static final int SELECTION_SELECTED = 2;
171     private static final int SELECTION_LONGPRESS = 3;
172
173     // Modulo used to pack (width,height) into a unique integer
174     private static final int MODULO_SHIFT = 16;
175
176     private int mSelectionMode = SELECTION_HIDDEN;
177
178     /**
179      * The first Julian day of the current month.
180      */
181     private int mFirstJulianDay;
182
183     private final EventLoader mEventLoader;
184
185     private ArrayList<Event> mEvents = new ArrayList<Event>();
186
187     private Drawable mTodayBackground;
188     private Drawable mDayBackground;
189
190     // Cached colors
191     private int mMonthOtherMonthColor;
192     private int mMonthWeekBannerColor;
193     private int mMonthOtherMonthBannerColor;
194     private int mMonthOtherMonthDayNumberColor;
195     private int mMonthDayNumberColor;
196     private int mMonthTodayNumberColor;
197
198     public MonthView(MonthActivity activity, Navigator navigator) {
199         super(activity);
200         if (mScale == 0) {
201             mScale = getContext().getResources().getDisplayMetrics().density;
202            if (mScale != 1) {
203                     WEEK_GAP *= mScale;
204                     MONTH_DAY_GAP *= mScale;
205                     HOUR_GAP *= mScale;
206
207                     MONTH_DAY_TEXT_SIZE *= mScale;
208                     WEEK_BANNER_HEIGHT *= mScale;
209                     WEEK_TEXT_SIZE *= mScale;
210                     WEEK_TEXT_PADDING *= mScale;
211                     BUSYBIT_WIDTH *= mScale;
212                     BUSYBIT_RIGHT_MARGIN *= mScale;
213                     BUSYBIT_TOP_BOTTOM_MARGIN *= mScale;
214
215                     HORIZONTAL_FLING_THRESHOLD *= mScale;
216                 }
217             }
218
219         mEventLoader = activity.mEventLoader;
220         mNavigator = navigator;
221         mEventGeometry = new EventGeometry();
222         mEventGeometry.setMinEventHeight(1.0f);
223         mEventGeometry.setHourGap(HOUR_GAP);
224         init(activity);
225     }
226
227     private void init(MonthActivity activity) {
228         setFocusable(true);
229         setClickable(true);
230         setOnCreateContextMenuListener(this);
231         mParentActivity = activity;
232         mViewCalendar = new Time();
233         long now = System.currentTimeMillis();
234         mViewCalendar.set(now);
235         mViewCalendar.monthDay = 1;
236         long millis = mViewCalendar.normalize(true /* ignore DST */);
237         mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff);
238         mViewCalendar.set(now);
239
240         mCursor = new DayOfMonthCursor(mViewCalendar.year,  mViewCalendar.month,
241                 mViewCalendar.monthDay, mParentActivity.getStartDay());
242         mToday = new Time();
243         mToday.set(System.currentTimeMillis());
244
245         mResources = activity.getResources();
246         mBoxSelected = mResources.getDrawable(R.drawable.month_view_selected);
247         mBoxPressed = mResources.getDrawable(R.drawable.month_view_pressed);
248         mBoxLongPressed = mResources.getDrawable(R.drawable.month_view_longpress);
249
250         mDnaEmpty = mResources.getDrawable(R.drawable.dna_empty);
251         mDnaTop = mResources.getDrawable(R.drawable.dna_1_of_6);
252         mDnaMiddle = mResources.getDrawable(R.drawable.dna_2345_of_6);
253         mDnaBottom = mResources.getDrawable(R.drawable.dna_6_of_6);
254         mTodayBackground = mResources.getDrawable(R.drawable.month_view_today_background);
255         mDayBackground = mResources.getDrawable(R.drawable.month_view_background);
256
257         // Cache color lookups
258         Resources res = getResources();
259         mMonthOtherMonthColor = res.getColor(R.color.month_other_month);
260         mMonthWeekBannerColor = res.getColor(R.color.month_week_banner);
261         mMonthOtherMonthBannerColor = res.getColor(R.color.month_other_month_banner);
262         mMonthOtherMonthDayNumberColor = res.getColor(R.color.month_other_month_day_number);
263         mMonthDayNumberColor = res.getColor(R.color.month_day_number);
264         mMonthTodayNumberColor = res.getColor(R.color.month_today_number);
265
266         if (mShowToast) {
267             LayoutInflater inflater;
268             inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
269             mPopupView = inflater.inflate(R.layout.month_bubble, null);
270             mPopup = new PopupWindow(activity);
271             mPopup.setContentView(mPopupView);
272             Resources.Theme dialogTheme = getResources().newTheme();
273             dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
274             TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
275                 android.R.attr.windowBackground });
276             mPopup.setBackgroundDrawable(ta.getDrawable(0));
277             ta.recycle();
278         }
279
280         mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
281             @Override
282             public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
283                     float velocityY) {
284                 // The user might do a slow "fling" after touching the screen
285                 // and we don't want the long-press to pop up a context menu.
286                 // Setting mLaunchDayView to false prevents the long-press.
287                 mLaunchDayView = false;
288                 mSelectionMode = SELECTION_HIDDEN;
289
290                 int distanceX = Math.abs((int) e2.getX() - (int) e1.getX());
291                 int distanceY = Math.abs((int) e2.getY() - (int) e1.getY());
292                 if (distanceY < HORIZONTAL_FLING_THRESHOLD || distanceY < distanceX) {
293                     return false;
294                 }
295
296                 // Switch to a different month
297                 Time time = mOtherViewCalendar;
298                 time.set(mViewCalendar);
299                 if (velocityY < 0) {
300                     time.month += 1;
301                 } else {
302                     time.month -= 1;
303                 }
304                 time.normalize(true);
305                 mParentActivity.goTo(time, true);
306
307                 return true;
308             }
309
310             @Override
311             public boolean onDown(MotionEvent e) {
312                 mLaunchDayView = false;
313                 return true;
314             }
315
316             @Override
317             public void onShowPress(MotionEvent e) {
318                 int x = (int) e.getX();
319                 int y = (int) e.getY();
320                 int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight);
321                 int col = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth);
322                 if (row > 5) {
323                     row = 5;
324                 }
325                 if (col > 6) {
326                     col = 6;
327                 }
328
329                 // Launch the Day/Agenda view when the finger lifts up,
330                 // unless the finger moves before lifting up.
331                 mLaunchDayView = true;
332
333                 // Highlight the selected day.
334                 mCursor.setSelectedRowColumn(row, col);
335                 mSelectionMode = SELECTION_PRESSED;
336                 mRedrawScreen = true;
337                 invalidate();
338             }
339
340             @Override
341             public void onLongPress(MotionEvent e) {
342                 // If mLaunchDayView is true, then we haven't done any scrolling
343                 // after touching the screen, so allow long-press to proceed
344                 // with popping up the context menu.
345                 if (mLaunchDayView) {
346                     mLaunchDayView = false;
347                     mSelectionMode = SELECTION_LONGPRESS;
348                     mRedrawScreen = true;
349                     invalidate();
350                     performLongClick();
351                 }
352             }
353
354             @Override
355             public boolean onScroll(MotionEvent e1, MotionEvent e2,
356                     float distanceX, float distanceY) {
357                 // If the user moves his finger after touching, then do not
358                 // launch the Day view when he lifts his finger.  Also, turn
359                 // off the selection.
360                 mLaunchDayView = false;
361
362                 if (mSelectionMode != SELECTION_HIDDEN) {
363                     mSelectionMode = SELECTION_HIDDEN;
364                     mRedrawScreen = true;
365                     invalidate();
366                 }
367                 return true;
368             }
369
370             @Override
371             public boolean onSingleTapUp(MotionEvent e) {
372                 if (mLaunchDayView) {
373                     mSelectionMode = SELECTION_SELECTED;
374                     mRedrawScreen = true;
375                     invalidate();
376                     mLaunchDayView = false;
377                     int x = (int) e.getX();
378                     int y = (int) e.getY();
379                     long millis = getSelectedMillisFor(x, y);
380                     Utils.startActivity(getContext(), mDetailedView, millis);
381                 }
382
383                 return true;
384             }
385         });
386     }
387
388     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
389         MenuItem item;
390         
391         final long startMillis = getSelectedTimeInMillis();
392         final int flags = DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
393                 | DateUtils.FORMAT_ABBREV_MONTH;
394        
395         final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);    
396         menu.setHeaderTitle(title);
397         
398         item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
399         item.setOnMenuItemClickListener(mContextMenuHandler);
400         item.setIcon(android.R.drawable.ic_menu_day);
401         item.setAlphabeticShortcut('d');
402
403         item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
404         item.setOnMenuItemClickListener(mContextMenuHandler);
405         item.setIcon(android.R.drawable.ic_menu_agenda);
406         item.setAlphabeticShortcut('a');
407
408         item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
409         item.setOnMenuItemClickListener(mContextMenuHandler);
410         item.setIcon(android.R.drawable.ic_menu_add);
411         item.setAlphabeticShortcut('n');
412     }
413
414     private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
415         public boolean onMenuItemClick(MenuItem item) {
416             switch (item.getItemId()) {
417                 case MenuHelper.MENU_DAY: {
418                     long startMillis = getSelectedTimeInMillis();
419                     Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
420                     break;
421                 }
422                 case MenuHelper.MENU_AGENDA: {
423                     long startMillis = getSelectedTimeInMillis();
424                     Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
425                     break;
426                 }
427                 case MenuHelper.MENU_EVENT_CREATE: {
428                     long startMillis = getSelectedTimeInMillis();
429                     long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
430                     Intent intent = new Intent(Intent.ACTION_VIEW);
431                     intent.setClassName(mContext, EditEvent.class.getName());
432                     intent.putExtra(EVENT_BEGIN_TIME, startMillis);
433                     intent.putExtra(EVENT_END_TIME, endMillis);
434                     mParentActivity.startActivity(intent);
435                     break;
436                 }
437                 default: {
438                     return false;
439                 }
440             }
441             return true;
442         }
443     }
444
445     void reloadEvents() {
446         // Get the date for the beginning of the month
447         Time monthStart = mTempTime;
448         monthStart.set(mViewCalendar);
449         monthStart.monthDay = 1;
450         monthStart.hour = 0;
451         monthStart.minute = 0;
452         monthStart.second = 0;
453         long millis = monthStart.normalize(true /* ignore isDst */);
454         int startDay = Time.getJulianDay(millis, monthStart.gmtoff);
455
456         // Load the busy-bits in the background
457         mParentActivity.startProgressSpinner();
458         final long startMillis;
459         if (PROFILE_LOAD_TIME) {
460             startMillis = SystemClock.uptimeMillis();
461         } else {
462             // To avoid a compiler error that this variable might not be initialized.
463             startMillis = 0;
464         }
465         mEventLoader.loadBusyBitsInBackground(startDay, 31, mRawBusyBits, mAllDayCounts,
466                 new Runnable() {
467             public void run() {
468                 convertBusyBits();
469                 if (PROFILE_LOAD_TIME) {
470                     long endMillis = SystemClock.uptimeMillis();
471                     long elapsed = endMillis - startMillis;
472                     Log.i("Cal", (mViewCalendar.month+1) + "/" + mViewCalendar.year + " Month view load busybits: " + elapsed);
473                 }
474                 mRedrawScreen = true;
475                 mParentActivity.stopProgressSpinner();
476                 invalidate();
477             }
478         });
479     }
480
481     void animationStarted() {
482         mAnimating = true;
483     }
484
485     void animationFinished() {
486         mAnimating = false;
487         mRedrawScreen = true;
488         invalidate();
489     }
490
491     @Override
492     protected void onSizeChanged(int width, int height, int oldw, int oldh) {
493         drawingCalc(width, height);
494         // If the size changed, then we should rebuild the bitmaps...
495         clearBitmapCache();
496     }
497
498     @Override
499     protected void onDetachedFromWindow() {
500         super.onDetachedFromWindow();
501         // No need to hang onto the bitmaps...
502         clearBitmapCache();
503         if (mBitmap != null) {
504             mBitmap.recycle();
505         }
506     }
507
508     @Override
509     protected void onDraw(Canvas canvas) {
510         if (mRedrawScreen) {
511             if (mCanvas == null) {
512                 drawingCalc(getWidth(), getHeight());
513             }
514
515             // If we are zero-sized, the canvas will remain null so check again
516             if (mCanvas != null) {
517                 // Clear the background
518                 final Canvas bitmapCanvas = mCanvas;
519                 bitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
520                 doDraw(bitmapCanvas);
521                 mRedrawScreen = false;
522             }
523         }
524
525         // If we are zero-sized, the bitmap will be null so guard against this
526         if (mBitmap != null) {
527             canvas.drawBitmap(mBitmap, mBitmapRect, mBitmapRect, null);
528         }
529     }
530
531     private void doDraw(Canvas canvas) {
532         boolean isLandscape = getResources().getConfiguration().orientation
533                 == Configuration.ORIENTATION_LANDSCAPE;
534
535         Paint p = new Paint();
536         Rect r = mRect;
537         int columnDay1 = mCursor.getColumnOf(1);
538
539         // Get the Julian day for the date at row 0, column 0.
540         int day = mFirstJulianDay - columnDay1;
541
542         int weekNum = 0;
543         Calendar calendar = null;
544         if (mShowWeekNumbers) {
545             calendar = Calendar.getInstance();
546             boolean noPrevMonth = (columnDay1 == 0);
547
548             // Compute the week number for the first row.
549             weekNum = getWeekOfYear(0, 0, noPrevMonth, calendar);
550         }
551
552         for (int row = 0; row < 6; row++) {
553             for (int column = 0; column < 7; column++) {
554                 drawBox(day, weekNum, row, column, canvas, p, r, isLandscape);
555                 day += 1;
556             }
557
558             if (mShowWeekNumbers) {
559                 weekNum += 1;
560                 if (weekNum >= 53) {
561                     boolean inCurrentMonth = (day - mFirstJulianDay < 31);
562                     weekNum = getWeekOfYear(row + 1, 0, inCurrentMonth, calendar);
563                 }
564             }
565         }
566         
567         drawGrid(canvas, p);
568     }
569
570     @Override
571     public boolean onTouchEvent(MotionEvent event) {
572         if (mGestureDetector.onTouchEvent(event)) {
573             return true;
574         }
575
576         return super.onTouchEvent(event);
577     }
578
579     private long getSelectedMillisFor(int x, int y) {
580         int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight);
581         int column = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth);
582         if (column > 6) {
583             column = 6;
584         }
585
586         DayOfMonthCursor c = mCursor;
587         Time time = mTempTime;
588         time.set(mViewCalendar);
589
590         // Compute the day number from the row and column.  If the row and
591         // column are in a different month from the current one, then the
592         // monthDay might be negative or it might be greater than the number
593         // of days in this month, but that is okay because the normalize()
594         // method will adjust the month (and year) if necessary.
595         time.monthDay = 7 * row + column - c.getOffset() + 1;
596         return time.normalize(true);
597     }
598
599     /**
600      * Create a bitmap at the origin and draw the drawable to it using the bounds specified by rect.
601      *
602      * @param drawable the drawable we wish to render
603      * @param width the width of the resulting bitmap
604      * @param height the height of the resulting bitmap
605      * @return a new bitmap
606      */
607     private Bitmap createBitmap(Drawable drawable, int width, int height) {
608         // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
609         Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
610
611         // Draw the drawable into the bitmap at the origin.
612         Canvas canvas = new Canvas(bitmap);
613         drawable.setBounds(0, 0, width, height);
614         drawable.draw(canvas);
615         return bitmap;
616     }
617
618     /**
619      * Clears the bitmap cache. Generally only needed when the screen size changed.
620      */
621     private void clearBitmapCache() {
622         recycleAndClearBitmapCache(mEventBitmapCache);
623         recycleAndClearBitmapCache(mDayBitmapCache);
624     }
625
626     private void recycleAndClearBitmapCache(SparseArray<Bitmap> bitmapCache) {
627         int size = bitmapCache.size();
628         for(int i = 0; i < size; i++) {
629             bitmapCache.valueAt(i).recycle();
630         }
631         bitmapCache.clear();
632
633     }
634
635     /**
636      * Draw the grid lines for the calendar
637      * @param canvas The canvas to draw on.
638      * @param p The paint used for drawing.
639      */
640     private void drawGrid(Canvas canvas, Paint p) {
641         p.setColor(mMonthOtherMonthColor);
642         p.setAntiAlias(false);
643         
644         final int width = getMeasuredWidth();
645         final int height = getMeasuredHeight();
646         
647         for (int row = 0; row < 6; row++) {
648             int y = WEEK_GAP + row * (WEEK_GAP + mCellHeight) - 1;
649             canvas.drawLine(0, y, width, y, p);
650         }
651         for (int column = 1; column < 7; column++) {
652             int x = mBorder + column * (MONTH_DAY_GAP + mCellWidth) - 1;
653             canvas.drawLine(x, WEEK_GAP, x, height, p);
654         }
655     }
656     
657     /**
658      * Draw a single box onto the canvas.
659      * @param day The Julian day.
660      * @param weekNum The week number.
661      * @param row The row of the box (0-5).
662      * @param column The column of the box (0-6).
663      * @param canvas The canvas to draw on.
664      * @param p The paint used for drawing.
665      * @param r The rectangle used for each box.
666      * @param isLandscape Is the current orientation landscape.
667      */
668     private void drawBox(int day, int weekNum, int row, int column, Canvas canvas, Paint p,
669             Rect r, boolean isLandscape) {
670
671         // Only draw the selection if we are in the press state or if we have
672         // moved the cursor with key input.
673         boolean drawSelection = false;
674         if (mSelectionMode != SELECTION_HIDDEN) {
675             drawSelection = mCursor.isSelected(row, column);
676         }
677
678         boolean withinCurrentMonth = mCursor.isWithinCurrentMonth(row, column);
679         boolean isToday = false;
680         int dayOfBox = mCursor.getDayAt(row, column);
681         if (dayOfBox == mToday.monthDay && mCursor.getYear() == mToday.year
682                 && mCursor.getMonth() == mToday.month) {
683             isToday = true;
684         }
685
686         int y = WEEK_GAP + row*(WEEK_GAP + mCellHeight);
687         int x = mBorder + column*(MONTH_DAY_GAP + mCellWidth);
688
689         r.left = x;
690         r.top = y;
691         r.right = x + mCellWidth;
692         r.bottom = y + mCellHeight;
693
694
695         // Adjust the left column, right column, and bottom row to leave
696         // no border.
697         if (column == 0) {
698             r.left = -1;
699         } else if (column == 6) {
700             r.right += mBorder + 2;
701         }
702
703         if (row == 5) {
704             r.bottom = getMeasuredHeight();
705         }
706
707         // Draw the cell contents (excluding monthDay number)
708         if (!withinCurrentMonth) {
709             boolean firstDayOfNextmonth = isFirstDayOfNextMonth(row, column);
710
711             // Adjust cell boundaries to compensate for the different border
712             // style.
713             r.top--;
714             if (column != 0) {
715                 r.left--;
716             }
717         } else if (drawSelection) {
718             if (mSelectionMode == SELECTION_SELECTED) {
719                 mBoxSelected.setBounds(r);
720                 mBoxSelected.draw(canvas);
721             } else if (mSelectionMode == SELECTION_PRESSED) {
722                 mBoxPressed.setBounds(r);
723                 mBoxPressed.draw(canvas);
724             } else {
725                 mBoxLongPressed.setBounds(r);
726                 mBoxLongPressed.draw(canvas);
727             }
728
729             drawEvents(day, canvas, r, p);
730             if (!mAnimating) {
731                 updateEventDetails(day);
732             }
733         } else {
734             // Today gets a different background
735             if (isToday) {
736                 // We could cache this for a little bit more performance, but it's not on the
737                 // performance radar...
738                 Drawable background = mTodayBackground;
739                 background.setBounds(r);
740                 background.draw(canvas);
741             } else {
742                 // Use the bitmap cache to draw the day background
743                 int width = r.right - r.left;
744                 int height = r.bottom - r.top;
745                 // Compute a unique id that depends on width and height.
746                 int id = (height << MODULO_SHIFT) | width;
747                 Bitmap bitmap = mDayBitmapCache.get(id);
748                 if (bitmap == null) {
749                      bitmap = createBitmap(mDayBackground, width, height);
750                      mDayBitmapCache.put(id, bitmap);
751                 }
752                 canvas.drawBitmap(bitmap, r.left, r.top, p);
753             }
754             drawEvents(day, canvas, r, p);
755         }
756
757         // Draw week number
758         if (mShowWeekNumbers && column == 0) {
759             // Draw the banner
760             p.setStyle(Paint.Style.FILL);
761             p.setColor(mMonthWeekBannerColor);
762             int right = r.right;
763             r.right = right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
764             if (isLandscape) {
765                 int bottom = r.bottom;
766                 r.bottom = r.top + WEEK_BANNER_HEIGHT;
767                 r.left++;
768                 canvas.drawRect(r, p);
769                 r.bottom = bottom;
770                 r.left--;
771             } else {
772                 int top = r.top;
773                 r.top = r.bottom - WEEK_BANNER_HEIGHT;
774                 r.left++;
775                 canvas.drawRect(r, p);
776                 r.top = top;
777                 r.left--;
778             }
779             r.right = right;
780
781             // Draw the number
782             p.setColor(mMonthOtherMonthBannerColor);
783             p.setAntiAlias(true);
784             p.setTypeface(null);
785             p.setTextSize(WEEK_TEXT_SIZE);
786             p.setTextAlign(Paint.Align.LEFT);
787
788             int textX = r.left + WEEK_TEXT_PADDING;
789             int textY;
790             if (isLandscape) {
791                 textY = r.top + WEEK_BANNER_HEIGHT - WEEK_TEXT_PADDING;
792             } else {
793                 textY = r.bottom - WEEK_TEXT_PADDING;
794             }
795
796             canvas.drawText(String.valueOf(weekNum), textX, textY, p);
797         }
798
799         // Draw the monthDay number
800         p.setStyle(Paint.Style.FILL);
801         p.setAntiAlias(true);
802         p.setTypeface(null);
803         p.setTextSize(MONTH_DAY_TEXT_SIZE);
804
805         if (!withinCurrentMonth) {
806             p.setColor(mMonthOtherMonthDayNumberColor);
807         } else if (drawSelection || !isToday) {
808             p.setColor(mMonthDayNumberColor);
809         } else {
810             p.setColor(mMonthTodayNumberColor);
811         }
812
813         p.setTextAlign(Paint.Align.CENTER);
814         int right = r.right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
815         int textX = r.left + (right - r.left) / 2; // center of text
816         int textY = r.bottom - BUSYBIT_TOP_BOTTOM_MARGIN - 2; // bottom of text
817         canvas.drawText(String.valueOf(mCursor.getDayAt(row, column)), textX, textY, p);
818     }
819
820     /**
821      * Converts the busy bits from the database that use 1-hour intervals to
822      * the 4-hour time slots needed in this view.  Also, we map all-day
823      * events to the first two 4-hour time slots (that is, an all-day event
824      * will look like the first 8 hours from 12am to 8am are busy).  This
825      * looks better than setting just the first 4-hour time slot because that
826      * is barely visible in landscape mode.
827      */
828     private void convertBusyBits() {
829         if (DEBUG_BUSYBITS) {
830             Log.i("Cal", "convertBusyBits() SLOTS_PER_DAY: " + SLOTS_PER_DAY
831                     + " BUSY_SLOT_MASK: " + BUSY_SLOT_MASK
832                     + " INTERVALS_PER_BUSY_SLOT: " + INTERVALS_PER_BUSY_SLOT);
833             for (int day = 0; day < 31; day++) {
834                 int bits = mRawBusyBits[day];
835                 String bitString = String.format("0x%06x", bits);
836                 String valString = "";
837                 for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
838                     int val = bits & BUSY_SLOT_MASK;
839                     bits = bits >>> INTERVALS_PER_BUSY_SLOT;
840                     valString += " " + val;
841                 }
842                 Log.i("Cal", "[" + day + "] " + bitString + " " + valString
843                         + " allday: " + mAllDayCounts[day]);
844             }
845         }
846         for (int day = 0; day < 31; day++) {
847             int bits = mRawBusyBits[day];
848             for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
849                 int val = bits & BUSY_SLOT_MASK;
850                 bits = bits >>> INTERVALS_PER_BUSY_SLOT;
851                 if (val == 0) {
852                     mBusyBits[day][slot] = 0;
853                 } else {
854                     mBusyBits[day][slot] = 1;
855                 }
856             }
857             if (mAllDayCounts[day] > 0) {
858                 mBusyBits[day][0] = 1;
859                 mBusyBits[day][1] = 1;
860             }
861         }
862     }
863
864     /**
865      * Create a bitmap at the origin for the given set of busyBits.
866      *
867      * @param busyBits an array of bits with elements set to 1 if we have an event for that slot
868      * @param rect the size of the resulting
869      * @return a new bitmap
870      */
871     private Bitmap createEventBitmap(byte[] busyBits, Rect rect) {
872         // Compute the size of the smallest bitmap, excluding margins.
873         final int left = 0;
874         final int right = BUSYBIT_WIDTH;
875         final int top = 0;
876         final int bottom = (rect.bottom - rect.top) - 2 * BUSYBIT_TOP_BOTTOM_MARGIN;
877         final int height = bottom - top;
878         final int width = right - left;
879
880         final Drawable dnaEmpty = mDnaEmpty;
881         final Drawable dnaTop = mDnaTop;
882         final Drawable dnaMiddle = mDnaMiddle;
883         final Drawable dnaBottom = mDnaBottom;
884         final float slotHeight = (float) height / SLOTS_PER_DAY;
885
886         // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
887         Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
888
889         // Create a canvas for drawing and draw background (dnaEmpty)
890         Canvas canvas = new Canvas(bitmap);
891         dnaEmpty.setBounds(left, top, right, bottom);
892         dnaEmpty.draw(canvas);
893
894         // The first busy bit is a drawable that is round at the top
895         if (busyBits[0] == 1) {
896             float rectBottom = top + slotHeight;
897             dnaTop.setBounds(left, top, right, (int) rectBottom);
898             dnaTop.draw(canvas);
899         }
900
901         // The last busy bit is a drawable that is round on the bottom
902         int lastIndex = busyBits.length - 1;
903         if (busyBits[lastIndex] == 1) {
904             float rectTop = bottom - slotHeight;
905             dnaBottom.setBounds(left, (int) rectTop, right, bottom);
906             dnaBottom.draw(canvas);
907         }
908
909         // Draw all intermediate pieces. We could further optimize this to
910         // draw runs of bits, but it probably won't yield much more performance.
911         float rectTop = top + slotHeight;
912         for (int index = 1; index < lastIndex; index++) {
913             float rectBottom = rectTop + slotHeight;
914             if (busyBits[index] == 1) {
915                 dnaMiddle.setBounds(left, (int) rectTop, right, (int) rectBottom);
916                 dnaMiddle.draw(canvas);
917             }
918             rectTop = rectBottom;
919         }
920         return bitmap;
921     }
922
923     private void drawEvents(int date, Canvas canvas, Rect rect, Paint p) {
924         // These are the coordinates of the upper left corner where we'll draw the event bitmap
925         int top = rect.top + BUSYBIT_TOP_BOTTOM_MARGIN;
926         int right = rect.right - BUSYBIT_RIGHT_MARGIN;
927         int left = right - BUSYBIT_WIDTH;
928
929         // Display the busy bits.  Draw a rectangle for each run of 1-bits.
930         int day = date - mFirstJulianDay;
931         byte[] busyBits = mBusyBits[day];
932         int lastIndex = busyBits.length - 1;
933
934         // Cache index is simply all of the bits combined into an integer
935         int cacheIndex = 0;
936         for (int i = 0 ; i <= lastIndex; i++) cacheIndex |= busyBits[i] << i;
937         Bitmap bitmap = mEventBitmapCache.get(cacheIndex);
938         if (bitmap == null) {
939             // Create a bitmap that we'll reuse for all events with the same
940             // combination of busyBits.
941             bitmap = createEventBitmap(busyBits, rect);
942             mEventBitmapCache.put(cacheIndex, bitmap);
943         }
944         canvas.drawBitmap(bitmap, left, top, p);
945     }
946
947     private boolean isFirstDayOfNextMonth(int row, int column) {
948         if (column == 0) {
949             column = 6;
950             row--;
951         } else {
952             column--;
953         }
954         return mCursor.isWithinCurrentMonth(row, column);
955     }
956
957     private int getWeekOfYear(int row, int column, boolean isWithinCurrentMonth,
958             Calendar calendar) {
959         calendar.set(Calendar.DAY_OF_MONTH, mCursor.getDayAt(row, column));
960         if (isWithinCurrentMonth) {
961             calendar.set(Calendar.MONTH, mCursor.getMonth());
962             calendar.set(Calendar.YEAR, mCursor.getYear());
963         } else {
964             int month = mCursor.getMonth();
965             int year = mCursor.getYear();
966             if (row < 2) {
967                 // Previous month
968                 if (month == 0) {
969                     year--;
970                     month = 11;
971                 } else {
972                     month--;
973                 }
974             } else {
975                 // Next month
976                 if (month == 11) {
977                     year++;
978                     month = 0;
979                 } else {
980                     month++;
981                 }
982             }
983             calendar.set(Calendar.MONTH, month);
984             calendar.set(Calendar.YEAR, year);
985         }
986
987         return calendar.get(Calendar.WEEK_OF_YEAR);
988     }
989
990     void setDetailedView(String detailedView) {
991         mDetailedView = detailedView;
992     }
993
994     void setSelectedTime(Time time) {
995         // Save the selected time so that we can restore it later when we switch views.
996         mSavedTime.set(time);
997
998         mViewCalendar.set(time);
999         mViewCalendar.monthDay = 1;
1000         long millis = mViewCalendar.normalize(true /* ignore DST */);
1001         mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff);
1002         mViewCalendar.set(time);
1003
1004         mCursor = new DayOfMonthCursor(time.year, time.month, time.monthDay,
1005                 mCursor.getWeekStartDay());
1006
1007         mRedrawScreen = true;
1008         invalidate();
1009     }
1010
1011     public long getSelectedTimeInMillis() {
1012         Time time = mTempTime;
1013         time.set(mViewCalendar);
1014
1015         time.month += mCursor.getSelectedMonthOffset();
1016         time.monthDay = mCursor.getSelectedDayOfMonth();
1017
1018         // Restore the saved hour:minute:second offset from when we entered
1019         // this view.
1020         time.second = mSavedTime.second;
1021         time.minute = mSavedTime.minute;
1022         time.hour = mSavedTime.hour;
1023         return time.normalize(true);
1024     }
1025
1026     Time getTime() {
1027         return mViewCalendar;
1028     }
1029
1030     public int getSelectionMode() {
1031         return mSelectionMode;
1032     }
1033
1034     public void setSelectionMode(int selectionMode) {
1035         mSelectionMode = selectionMode;
1036     }
1037
1038     private void drawingCalc(int width, int height) {
1039         mCellHeight = (height - (6 * WEEK_GAP)) / 6;
1040         mEventGeometry.setHourHeight((mCellHeight - 25.0f * HOUR_GAP) / 24.0f);
1041         mCellWidth = (width - (6 * MONTH_DAY_GAP)) / 7;
1042         mBorder = (width - 6 * (mCellWidth + MONTH_DAY_GAP) - mCellWidth) / 2;
1043
1044         if (mShowToast) {
1045             mPopup.dismiss();
1046             mPopup.setWidth(width - 20);
1047             mPopup.setHeight(POPUP_HEIGHT);
1048         }
1049
1050         if (((mBitmap == null)
1051                     || mBitmap.isRecycled()
1052                     || (mBitmap.getHeight() != height)
1053                     || (mBitmap.getWidth() != width))
1054                 && (width > 0) && (height > 0)) {
1055             if (mBitmap != null) {
1056                 mBitmap.recycle();
1057             }
1058             mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1059             mCanvas = new Canvas(mBitmap);
1060         }
1061
1062         mBitmapRect.top = 0;
1063         mBitmapRect.bottom = height;
1064         mBitmapRect.left = 0;
1065         mBitmapRect.right = width;
1066     }
1067
1068     private void updateEventDetails(int date) {
1069         if (!mShowToast) {
1070             return;
1071         }
1072
1073         getHandler().removeCallbacks(mDismissPopup);
1074         ArrayList<Event> events = mEvents;
1075         int numEvents = events.size();
1076         if (numEvents == 0) {
1077             mPopup.dismiss();
1078             return;
1079         }
1080
1081         int eventIndex = 0;
1082         for (int i = 0; i < numEvents; i++) {
1083             Event event = events.get(i);
1084
1085             if (event.startDay > date || event.endDay < date) {
1086                 continue;
1087             }
1088
1089             // If we have all the event that we can display, then just count
1090             // the extra ones.
1091             if (eventIndex >= 4) {
1092                 eventIndex += 1;
1093                 continue;
1094             }
1095
1096             int flags;
1097             boolean showEndTime = false;
1098             if (event.allDay) {
1099                 int numDays = event.endDay - event.startDay;
1100                 if (numDays == 0) {
1101                     flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
1102                             | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
1103                 } else {
1104                     showEndTime = true;
1105                     flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
1106                             | DateUtils.FORMAT_ABBREV_ALL;
1107                 }
1108             } else {
1109                 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1110                 if (DateFormat.is24HourFormat(mContext)) {
1111                     flags |= DateUtils.FORMAT_24HOUR;
1112                 }
1113             }
1114
1115             String timeRange;
1116             if (showEndTime) {
1117                 timeRange = DateUtils.formatDateRange(mParentActivity,
1118                         event.startMillis, event.endMillis, flags);
1119             } else {
1120                 timeRange = DateUtils.formatDateRange(mParentActivity,
1121                         event.startMillis, event.startMillis, flags);
1122             }
1123
1124             TextView timeView = null;
1125             TextView titleView = null;
1126             switch (eventIndex) {
1127                 case 0:
1128                     timeView = (TextView) mPopupView.findViewById(R.id.time0);
1129                     titleView = (TextView) mPopupView.findViewById(R.id.event_title0);
1130                     break;
1131                 case 1:
1132                     timeView = (TextView) mPopupView.findViewById(R.id.time1);
1133                     titleView = (TextView) mPopupView.findViewById(R.id.event_title1);
1134                     break;
1135                 case 2:
1136                     timeView = (TextView) mPopupView.findViewById(R.id.time2);
1137                     titleView = (TextView) mPopupView.findViewById(R.id.event_title2);
1138                     break;
1139                 case 3:
1140                     timeView = (TextView) mPopupView.findViewById(R.id.time3);
1141                     titleView = (TextView) mPopupView.findViewById(R.id.event_title3);
1142                     break;
1143             }
1144
1145             timeView.setText(timeRange);
1146             titleView.setText(event.title);
1147             eventIndex += 1;
1148         }
1149         if (eventIndex == 0) {
1150             // We didn't find any events for this day
1151             mPopup.dismiss();
1152             return;
1153         }
1154
1155         // Hide the items that have no event information
1156         View view;
1157         switch (eventIndex) {
1158             case 1:
1159                 view = mPopupView.findViewById(R.id.item_layout1);
1160                 view.setVisibility(View.GONE);
1161                 view = mPopupView.findViewById(R.id.item_layout2);
1162                 view.setVisibility(View.GONE);
1163                 view = mPopupView.findViewById(R.id.item_layout3);
1164                 view.setVisibility(View.GONE);
1165                 view = mPopupView.findViewById(R.id.plus_more);
1166                 view.setVisibility(View.GONE);
1167                 break;
1168             case 2:
1169                 view = mPopupView.findViewById(R.id.item_layout1);
1170                 view.setVisibility(View.VISIBLE);
1171                 view = mPopupView.findViewById(R.id.item_layout2);
1172                 view.setVisibility(View.GONE);
1173                 view = mPopupView.findViewById(R.id.item_layout3);
1174                 view.setVisibility(View.GONE);
1175                 view = mPopupView.findViewById(R.id.plus_more);
1176                 view.setVisibility(View.GONE);
1177                 break;
1178             case 3:
1179                 view = mPopupView.findViewById(R.id.item_layout1);
1180                 view.setVisibility(View.VISIBLE);
1181                 view = mPopupView.findViewById(R.id.item_layout2);
1182                 view.setVisibility(View.VISIBLE);
1183                 view = mPopupView.findViewById(R.id.item_layout3);
1184                 view.setVisibility(View.GONE);
1185                 view = mPopupView.findViewById(R.id.plus_more);
1186                 view.setVisibility(View.GONE);
1187                 break;
1188             case 4:
1189                 view = mPopupView.findViewById(R.id.item_layout1);
1190                 view.setVisibility(View.VISIBLE);
1191                 view = mPopupView.findViewById(R.id.item_layout2);
1192                 view.setVisibility(View.VISIBLE);
1193                 view = mPopupView.findViewById(R.id.item_layout3);
1194                 view.setVisibility(View.VISIBLE);
1195                 view = mPopupView.findViewById(R.id.plus_more);
1196                 view.setVisibility(View.GONE);
1197                 break;
1198             default:
1199                 view = mPopupView.findViewById(R.id.item_layout1);
1200                 view.setVisibility(View.VISIBLE);
1201                 view = mPopupView.findViewById(R.id.item_layout2);
1202                 view.setVisibility(View.VISIBLE);
1203                 view = mPopupView.findViewById(R.id.item_layout3);
1204                 view.setVisibility(View.VISIBLE);
1205                 TextView tv = (TextView) mPopupView.findViewById(R.id.plus_more);
1206                 tv.setVisibility(View.VISIBLE);
1207                 String format = mResources.getString(R.string.plus_N_more);
1208                 String plusMore = String.format(format, eventIndex - 4);
1209                 tv.setText(plusMore);
1210                 break;
1211         }
1212
1213         if (eventIndex > 5) {
1214             eventIndex = 5;
1215         }
1216         int popupHeight = 20 * eventIndex + 15;
1217         mPopup.setHeight(popupHeight);
1218
1219         if (mPreviousPopupHeight != popupHeight) {
1220             mPreviousPopupHeight = popupHeight;
1221             mPopup.dismiss();
1222         }
1223         mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, 0, 0);
1224         postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
1225     }
1226
1227     @Override
1228     public boolean onKeyUp(int keyCode, KeyEvent event) {
1229         long duration = event.getEventTime() - event.getDownTime();
1230
1231         switch (keyCode) {
1232         case KeyEvent.KEYCODE_DPAD_CENTER:
1233             if (mSelectionMode == SELECTION_HIDDEN) {
1234                 // Don't do anything unless the selection is visible.
1235                 break;
1236             }
1237
1238             if (mSelectionMode == SELECTION_PRESSED) {
1239                 // This was the first press when there was nothing selected.
1240                 // Change the selection from the "pressed" state to the
1241                 // the "selected" state.  We treat short-press and
1242                 // long-press the same here because nothing was selected.
1243                 mSelectionMode = SELECTION_SELECTED;
1244                 mRedrawScreen = true;
1245                 invalidate();
1246                 break;
1247             }
1248
1249             // Check the duration to determine if this was a short press
1250             if (duration < ViewConfiguration.getLongPressTimeout()) {
1251                 long millis = getSelectedTimeInMillis();
1252                 Utils.startActivity(getContext(), mDetailedView, millis);
1253             } else {
1254                 mSelectionMode = SELECTION_LONGPRESS;
1255                 mRedrawScreen = true;
1256                 invalidate();
1257                 performLongClick();
1258             }
1259         }
1260         return super.onKeyUp(keyCode, event);
1261     }
1262
1263     @Override
1264     public boolean onKeyDown(int keyCode, KeyEvent event) {
1265         if (mSelectionMode == SELECTION_HIDDEN) {
1266             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1267                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
1268                     || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
1269                 // Display the selection box but don't move or select it
1270                 // on this key press.
1271                 mSelectionMode = SELECTION_SELECTED;
1272                 mRedrawScreen = true;
1273                 invalidate();
1274                 return true;
1275             } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
1276                 // Display the selection box but don't select it
1277                 // on this key press.
1278                 mSelectionMode = SELECTION_PRESSED;
1279                 mRedrawScreen = true;
1280                 invalidate();
1281                 return true;
1282             }
1283         }
1284
1285         mSelectionMode = SELECTION_SELECTED;
1286         boolean redraw = false;
1287         Time other = null;
1288
1289         switch (keyCode) {
1290         case KeyEvent.KEYCODE_ENTER:
1291             long millis = getSelectedTimeInMillis();
1292             Utils.startActivity(getContext(), mDetailedView, millis);
1293             return true;
1294         case KeyEvent.KEYCODE_DPAD_UP:
1295             if (mCursor.up()) {
1296                 other = mOtherViewCalendar;
1297                 other.set(mViewCalendar);
1298                 other.month -= 1;
1299                 other.monthDay = mCursor.getSelectedDayOfMonth();
1300
1301                 // restore the calendar cursor for the animation
1302                 mCursor.down();
1303             }
1304             redraw = true;
1305             break;
1306
1307         case KeyEvent.KEYCODE_DPAD_DOWN:
1308             if (mCursor.down()) {
1309                 other = mOtherViewCalendar;
1310                 other.set(mViewCalendar);
1311                 other.month += 1;
1312                 other.monthDay = mCursor.getSelectedDayOfMonth();
1313
1314                 // restore the calendar cursor for the animation
1315                 mCursor.up();
1316             }
1317             redraw = true;
1318             break;
1319
1320         case KeyEvent.KEYCODE_DPAD_LEFT:
1321             if (mCursor.left()) {
1322                 other = mOtherViewCalendar;
1323                 other.set(mViewCalendar);
1324                 other.month -= 1;
1325                 other.monthDay = mCursor.getSelectedDayOfMonth();
1326
1327                 // restore the calendar cursor for the animation
1328                 mCursor.right();
1329             }
1330             redraw = true;
1331             break;
1332
1333         case KeyEvent.KEYCODE_DPAD_RIGHT:
1334             if (mCursor.right()) {
1335                 other = mOtherViewCalendar;
1336                 other.set(mViewCalendar);
1337                 other.month += 1;
1338                 other.monthDay = mCursor.getSelectedDayOfMonth();
1339
1340                 // restore the calendar cursor for the animation
1341                 mCursor.left();
1342             }
1343             redraw = true;
1344             break;
1345         }
1346
1347         if (other != null) {
1348             other.normalize(true /* ignore DST */);
1349             mNavigator.goTo(other, true);
1350         } else if (redraw) {
1351             mRedrawScreen = true;
1352             invalidate();
1353         }
1354
1355         return redraw;
1356     }
1357
1358     class DismissPopup implements Runnable {
1359         public void run() {
1360             mPopup.dismiss();
1361         }
1362     }
1363
1364     // This is called when the activity is paused so that the popup can
1365     // be dismissed.
1366     void dismissPopup() {
1367         if (!mShowToast) {
1368             return;
1369         }
1370
1371         // Protect against null-pointer exceptions
1372         if (mPopup != null) {
1373             mPopup.dismiss();
1374         }
1375
1376         Handler handler = getHandler();
1377         if (handler != null) {
1378             handler.removeCallbacks(mDismissPopup);
1379         }
1380     }
1381 }