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