]> nv-tegra.nvidia Code Review - android/platform/packages/apps/Calendar.git/blob - src/com/android/calendar/MonthView.java
auto import from //depot/cupcake/@132589
[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 = 0;
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(getContext(), 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         drawGrid(canvas, p);
551     }
552
553     @Override
554     public boolean onTouchEvent(MotionEvent event) {
555         if (mGestureDetector.onTouchEvent(event)) {
556             return true;
557         }
558
559         return super.onTouchEvent(event);
560     }
561
562     private long getSelectedMillisFor(int x, int y) {
563         int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight);
564         int column = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth);
565         if (column > 6) {
566             column = 6;
567         }
568
569         DayOfMonthCursor c = mCursor;
570         Time time = mTempTime;
571         time.set(mViewCalendar);
572
573         // Compute the day number from the row and column.  If the row and
574         // column are in a different month from the current one, then the
575         // monthDay might be negative or it might be greater than the number
576         // of days in this month, but that is okay because the normalize()
577         // method will adjust the month (and year) if necessary.
578         time.monthDay = 7 * row + column - c.getOffset() + 1;
579         return time.normalize(true);
580     }
581
582     /**
583      * Create a bitmap at the origin and draw the drawable to it using the bounds specified by rect.
584      *
585      * @param drawable the drawable we wish to render
586      * @param width the width of the resulting bitmap
587      * @param height the height of the resulting bitmap
588      * @return a new bitmap
589      */
590     private Bitmap createBitmap(Drawable drawable, int width, int height) {
591         // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
592         Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
593
594         // Draw the drawable into the bitmap at the origin.
595         Canvas canvas = new Canvas(bitmap);
596         drawable.setBounds(0, 0, width, height);
597         drawable.draw(canvas);
598         return bitmap;
599     }
600
601     /**
602      * Clears the bitmap cache. Generally only needed when the screen size changed.
603      */
604     private void clearBitmapCache() {
605         recycleAndClearBitmapCache(mEventBitmapCache);
606         recycleAndClearBitmapCache(mDayBitmapCache);
607     }
608
609     private void recycleAndClearBitmapCache(SparseArray<Bitmap> bitmapCache) {
610         int size = bitmapCache.size();
611         for(int i = 0; i < size; i++) {
612             bitmapCache.valueAt(i).recycle();
613         }
614         bitmapCache.clear();
615
616     }
617
618     /**
619      * Draw the grid lines for the calendar
620      * @param canvas The canvas to draw on.
621      * @param p The paint used for drawing.
622      */
623     private void drawGrid(Canvas canvas, Paint p) {
624         p.setColor(mMonthOtherMonthColor);
625         p.setAntiAlias(false);
626         
627         final int width = getMeasuredWidth();
628         final int height = getMeasuredHeight();
629         
630         for (int row = 0; row < 6; row++) {
631             int y = WEEK_GAP + row * (WEEK_GAP + mCellHeight) - 1;
632             canvas.drawLine(0, y, width, y, p);
633         }
634         for (int column = 1; column < 7; column++) {
635             int x = mBorder + column * (MONTH_DAY_GAP + mCellWidth) - 1;
636             canvas.drawLine(x, WEEK_GAP, x, height, p);
637         }
638     }
639     
640     /**
641      * Draw a single box onto the canvas.
642      * @param day The Julian day.
643      * @param weekNum The week number.
644      * @param row The row of the box (0-5).
645      * @param column The column of the box (0-6).
646      * @param canvas The canvas to draw on.
647      * @param p The paint used for drawing.
648      * @param r The rectangle used for each box.
649      * @param isLandscape Is the current orientation landscape.
650      */
651     private void drawBox(int day, int weekNum, int row, int column, Canvas canvas, Paint p,
652             Rect r, boolean isLandscape) {
653
654         // Only draw the selection if we are in the press state or if we have
655         // moved the cursor with key input.
656         boolean drawSelection = false;
657         if (mSelectionMode != SELECTION_HIDDEN) {
658             drawSelection = mCursor.isSelected(row, column);
659         }
660
661         boolean withinCurrentMonth = mCursor.isWithinCurrentMonth(row, column);
662         boolean isToday = false;
663         int dayOfBox = mCursor.getDayAt(row, column);
664         if (dayOfBox == mToday.monthDay && mCursor.getYear() == mToday.year
665                 && mCursor.getMonth() == mToday.month) {
666             isToday = true;
667         }
668
669         int y = WEEK_GAP + row*(WEEK_GAP + mCellHeight);
670         int x = mBorder + column*(MONTH_DAY_GAP + mCellWidth);
671
672         r.left = x;
673         r.top = y;
674         r.right = x + mCellWidth;
675         r.bottom = y + mCellHeight;
676
677
678         // Adjust the left column, right column, and bottom row to leave
679         // no border.
680         if (column == 0) {
681             r.left = -1;
682         } else if (column == 6) {
683             r.right += mBorder + 2;
684         }
685
686         if (row == 5) {
687             r.bottom = getMeasuredHeight();
688         }
689
690         // Draw the cell contents (excluding monthDay number)
691         if (!withinCurrentMonth) {
692             boolean firstDayOfNextmonth = isFirstDayOfNextMonth(row, column);
693
694             // Adjust cell boundaries to compensate for the different border
695             // style.
696             r.top--;
697             if (column != 0) {
698                 r.left--;
699             }
700         } else if (drawSelection) {
701             if (mSelectionMode == SELECTION_SELECTED) {
702                 mBoxSelected.setBounds(r);
703                 mBoxSelected.draw(canvas);
704             } else if (mSelectionMode == SELECTION_PRESSED) {
705                 mBoxPressed.setBounds(r);
706                 mBoxPressed.draw(canvas);
707             } else {
708                 mBoxLongPressed.setBounds(r);
709                 mBoxLongPressed.draw(canvas);
710             }
711
712             drawEvents(day, canvas, r, p);
713             if (!mAnimating) {
714                 updateEventDetails(day);
715             }
716         } else {
717             // Today gets a different background
718             if (isToday) {
719                 // We could cache this for a little bit more performance, but it's not on the
720                 // performance radar...
721                 Drawable background = mTodayBackground;
722                 background.setBounds(r);
723                 background.draw(canvas);
724             } else {
725                 // Use the bitmap cache to draw the day background
726                 int width = r.right - r.left;
727                 int height = r.bottom - r.top;
728                 // Compute a unique id that depends on width and height.
729                 int id = (height << MODULO_SHIFT) | width;
730                 Bitmap bitmap = mDayBitmapCache.get(id);
731                 if (bitmap == null) {
732                      bitmap = createBitmap(mDayBackground, width, height);
733                      mDayBitmapCache.put(id, bitmap);
734                 }
735                 canvas.drawBitmap(bitmap, r.left, r.top, p);
736             }
737             drawEvents(day, canvas, r, p);
738         }
739
740         // Draw week number
741         if (mShowWeekNumbers && column == 0) {
742             // Draw the banner
743             p.setStyle(Paint.Style.FILL);
744             p.setColor(mMonthWeekBannerColor);
745             int right = r.right;
746             r.right = right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
747             if (isLandscape) {
748                 int bottom = r.bottom;
749                 r.bottom = r.top + WEEK_BANNER_HEIGHT;
750                 r.left++;
751                 canvas.drawRect(r, p);
752                 r.bottom = bottom;
753                 r.left--;
754             } else {
755                 int top = r.top;
756                 r.top = r.bottom - WEEK_BANNER_HEIGHT;
757                 r.left++;
758                 canvas.drawRect(r, p);
759                 r.top = top;
760                 r.left--;
761             }
762             r.right = right;
763
764             // Draw the number
765             p.setColor(mMonthOtherMonthBannerColor);
766             p.setAntiAlias(true);
767             p.setTypeface(null);
768             p.setTextSize(WEEK_TEXT_SIZE);
769             p.setTextAlign(Paint.Align.LEFT);
770
771             int textX = r.left + WEEK_TEXT_PADDING;
772             int textY;
773             if (isLandscape) {
774                 textY = r.top + WEEK_BANNER_HEIGHT - WEEK_TEXT_PADDING;
775             } else {
776                 textY = r.bottom - WEEK_TEXT_PADDING;
777             }
778
779             canvas.drawText(String.valueOf(weekNum), textX, textY, p);
780         }
781
782         // Draw the monthDay number
783         p.setStyle(Paint.Style.FILL);
784         p.setAntiAlias(true);
785         p.setTypeface(null);
786         p.setTextSize(MONTH_DAY_TEXT_SIZE);
787
788         if (!withinCurrentMonth) {
789             p.setColor(mMonthOtherMonthDayNumberColor);
790         } else if (drawSelection || !isToday) {
791             p.setColor(mMonthDayNumberColor);
792         } else {
793             p.setColor(mMonthTodayNumberColor);
794         }
795
796         p.setTextAlign(Paint.Align.CENTER);
797         int right = r.right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN;
798         int textX = r.left + (right - r.left) / 2; // center of text
799         int textY = r.bottom - BUSYBIT_TOP_BOTTOM_MARGIN - 2; // bottom of text
800         canvas.drawText(String.valueOf(mCursor.getDayAt(row, column)), textX, textY, p);
801     }
802
803     /**
804      * Converts the busy bits from the database that use 1-hour intervals to
805      * the 4-hour time slots needed in this view.  Also, we map all-day
806      * events to the first two 4-hour time slots (that is, an all-day event
807      * will look like the first 8 hours from 12am to 8am are busy).  This
808      * looks better than setting just the first 4-hour time slot because that
809      * is barely visible in landscape mode.
810      */
811     private void convertBusyBits() {
812         if (DEBUG_BUSYBITS) {
813             Log.i("Cal", "convertBusyBits() SLOTS_PER_DAY: " + SLOTS_PER_DAY
814                     + " BUSY_SLOT_MASK: " + BUSY_SLOT_MASK
815                     + " INTERVALS_PER_BUSY_SLOT: " + INTERVALS_PER_BUSY_SLOT);
816             for (int day = 0; day < 31; day++) {
817                 int bits = mRawBusyBits[day];
818                 String bitString = String.format("0x%06x", bits);
819                 String valString = "";
820                 for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
821                     int val = bits & BUSY_SLOT_MASK;
822                     bits = bits >>> INTERVALS_PER_BUSY_SLOT;
823                     valString += " " + val;
824                 }
825                 Log.i("Cal", "[" + day + "] " + bitString + " " + valString
826                         + " allday: " + mAllDayCounts[day]);
827             }
828         }
829         for (int day = 0; day < 31; day++) {
830             int bits = mRawBusyBits[day];
831             for (int slot = 0; slot < SLOTS_PER_DAY; slot++) {
832                 int val = bits & BUSY_SLOT_MASK;
833                 bits = bits >>> INTERVALS_PER_BUSY_SLOT;
834                 if (val == 0) {
835                     mBusyBits[day][slot] = 0;
836                 } else {
837                     mBusyBits[day][slot] = 1;
838                 }
839             }
840             if (mAllDayCounts[day] > 0) {
841                 mBusyBits[day][0] = 1;
842                 mBusyBits[day][1] = 1;
843             }
844         }
845     }
846
847     /**
848      * Create a bitmap at the origin for the given set of busyBits.
849      *
850      * @param busyBits an array of bits with elements set to 1 if we have an event for that slot
851      * @param rect the size of the resulting
852      * @return a new bitmap
853      */
854     private Bitmap createEventBitmap(byte[] busyBits, Rect rect) {
855         // Compute the size of the smallest bitmap, excluding margins.
856         final int left = 0;
857         final int right = BUSYBIT_WIDTH;
858         final int top = 0;
859         final int bottom = (rect.bottom - rect.top) - 2 * BUSYBIT_TOP_BOTTOM_MARGIN;
860         final int height = bottom - top;
861         final int width = right - left;
862
863         final Drawable dnaEmpty = mDnaEmpty;
864         final Drawable dnaTop = mDnaTop;
865         final Drawable dnaMiddle = mDnaMiddle;
866         final Drawable dnaBottom = mDnaBottom;
867         final float slotHeight = (float) height / SLOTS_PER_DAY;
868
869         // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888)
870         Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig());
871
872         // Create a canvas for drawing and draw background (dnaEmpty)
873         Canvas canvas = new Canvas(bitmap);
874         dnaEmpty.setBounds(left, top, right, bottom);
875         dnaEmpty.draw(canvas);
876
877         // The first busy bit is a drawable that is round at the top
878         if (busyBits[0] == 1) {
879             float rectBottom = top + slotHeight;
880             dnaTop.setBounds(left, top, right, (int) rectBottom);
881             dnaTop.draw(canvas);
882         }
883
884         // The last busy bit is a drawable that is round on the bottom
885         int lastIndex = busyBits.length - 1;
886         if (busyBits[lastIndex] == 1) {
887             float rectTop = bottom - slotHeight;
888             dnaBottom.setBounds(left, (int) rectTop, right, bottom);
889             dnaBottom.draw(canvas);
890         }
891
892         // Draw all intermediate pieces. We could further optimize this to
893         // draw runs of bits, but it probably won't yield much more performance.
894         float rectTop = top + slotHeight;
895         for (int index = 1; index < lastIndex; index++) {
896             float rectBottom = rectTop + slotHeight;
897             if (busyBits[index] == 1) {
898                 dnaMiddle.setBounds(left, (int) rectTop, right, (int) rectBottom);
899                 dnaMiddle.draw(canvas);
900             }
901             rectTop = rectBottom;
902         }
903         return bitmap;
904     }
905
906     private void drawEvents(int date, Canvas canvas, Rect rect, Paint p) {
907         // These are the coordinates of the upper left corner where we'll draw the event bitmap
908         int top = rect.top + BUSYBIT_TOP_BOTTOM_MARGIN;
909         int right = rect.right - BUSYBIT_RIGHT_MARGIN;
910         int left = right - BUSYBIT_WIDTH;
911
912         // Display the busy bits.  Draw a rectangle for each run of 1-bits.
913         int day = date - mFirstJulianDay;
914         byte[] busyBits = mBusyBits[day];
915         int lastIndex = busyBits.length - 1;
916
917         // Cache index is simply all of the bits combined into an integer
918         int cacheIndex = 0;
919         for (int i = 0 ; i <= lastIndex; i++) cacheIndex |= busyBits[i] << i;
920         Bitmap bitmap = mEventBitmapCache.get(cacheIndex);
921         if (bitmap == null) {
922             // Create a bitmap that we'll reuse for all events with the same
923             // combination of busyBits.
924             bitmap = createEventBitmap(busyBits, rect);
925             mEventBitmapCache.put(cacheIndex, bitmap);
926         }
927         canvas.drawBitmap(bitmap, left, top, p);
928     }
929
930     private boolean isFirstDayOfNextMonth(int row, int column) {
931         if (column == 0) {
932             column = 6;
933             row--;
934         } else {
935             column--;
936         }
937         return mCursor.isWithinCurrentMonth(row, column);
938     }
939
940     private int getWeekOfYear(int row, int column, boolean isWithinCurrentMonth,
941             Calendar calendar) {
942         calendar.set(Calendar.DAY_OF_MONTH, mCursor.getDayAt(row, column));
943         if (isWithinCurrentMonth) {
944             calendar.set(Calendar.MONTH, mCursor.getMonth());
945             calendar.set(Calendar.YEAR, mCursor.getYear());
946         } else {
947             int month = mCursor.getMonth();
948             int year = mCursor.getYear();
949             if (row < 2) {
950                 // Previous month
951                 if (month == 0) {
952                     year--;
953                     month = 11;
954                 } else {
955                     month--;
956                 }
957             } else {
958                 // Next month
959                 if (month == 11) {
960                     year++;
961                     month = 0;
962                 } else {
963                     month++;
964                 }
965             }
966             calendar.set(Calendar.MONTH, month);
967             calendar.set(Calendar.YEAR, year);
968         }
969
970         return calendar.get(Calendar.WEEK_OF_YEAR);
971     }
972
973     void setDetailedView(String detailedView) {
974         mDetailedView = detailedView;
975     }
976
977     void setSelectedTime(Time time) {
978         // Save the selected time so that we can restore it later when we switch views.
979         mSavedTime.set(time);
980
981         mViewCalendar.set(time);
982         mViewCalendar.monthDay = 1;
983         long millis = mViewCalendar.normalize(true /* ignore DST */);
984         mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff);
985         mViewCalendar.set(time);
986
987         mCursor = new DayOfMonthCursor(time.year, time.month, time.monthDay,
988                 mCursor.getWeekStartDay());
989
990         mRedrawScreen = true;
991         invalidate();
992     }
993
994     public long getSelectedTimeInMillis() {
995         Time time = mTempTime;
996         time.set(mViewCalendar);
997
998         time.month += mCursor.getSelectedMonthOffset();
999         time.monthDay = mCursor.getSelectedDayOfMonth();
1000
1001         // Restore the saved hour:minute:second offset from when we entered
1002         // this view.
1003         time.second = mSavedTime.second;
1004         time.minute = mSavedTime.minute;
1005         time.hour = mSavedTime.hour;
1006         return time.normalize(true);
1007     }
1008
1009     Time getTime() {
1010         return mViewCalendar;
1011     }
1012
1013     public int getSelectionMode() {
1014         return mSelectionMode;
1015     }
1016
1017     public void setSelectionMode(int selectionMode) {
1018         mSelectionMode = selectionMode;
1019     }
1020
1021     private void drawingCalc(int width, int height) {
1022         mCellHeight = (height - (6 * WEEK_GAP)) / 6;
1023         mEventGeometry.setHourHeight((mCellHeight - 25.0f * HOUR_GAP) / 24.0f);
1024         mCellWidth = (width - (6 * MONTH_DAY_GAP)) / 7;
1025         mBorder = (width - 6 * (mCellWidth + MONTH_DAY_GAP) - mCellWidth) / 2;
1026
1027         if (mShowToast) {
1028             mPopup.dismiss();
1029             mPopup.setWidth(width - 20);
1030             mPopup.setHeight(POPUP_HEIGHT);
1031         }
1032
1033         if (((mBitmap == null)
1034                     || mBitmap.isRecycled()
1035                     || (mBitmap.getHeight() != height)
1036                     || (mBitmap.getWidth() != width))
1037                 && (width > 0) && (height > 0)) {
1038             if (mBitmap != null) {
1039                 mBitmap.recycle();
1040             }
1041             mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1042             mCanvas = new Canvas(mBitmap);
1043         }
1044
1045         mBitmapRect.top = 0;
1046         mBitmapRect.bottom = height;
1047         mBitmapRect.left = 0;
1048         mBitmapRect.right = width;
1049     }
1050
1051     private void updateEventDetails(int date) {
1052         if (!mShowToast) {
1053             return;
1054         }
1055
1056         getHandler().removeCallbacks(mDismissPopup);
1057         ArrayList<Event> events = mEvents;
1058         int numEvents = events.size();
1059         if (numEvents == 0) {
1060             mPopup.dismiss();
1061             return;
1062         }
1063
1064         int eventIndex = 0;
1065         for (int i = 0; i < numEvents; i++) {
1066             Event event = events.get(i);
1067
1068             if (event.startDay > date || event.endDay < date) {
1069                 continue;
1070             }
1071
1072             // If we have all the event that we can display, then just count
1073             // the extra ones.
1074             if (eventIndex >= 4) {
1075                 eventIndex += 1;
1076                 continue;
1077             }
1078
1079             int flags;
1080             boolean showEndTime = false;
1081             if (event.allDay) {
1082                 int numDays = event.endDay - event.startDay;
1083                 if (numDays == 0) {
1084                     flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
1085                             | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
1086                 } else {
1087                     showEndTime = true;
1088                     flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE
1089                             | DateUtils.FORMAT_ABBREV_ALL;
1090                 }
1091             } else {
1092                 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1093                 if (DateFormat.is24HourFormat(mContext)) {
1094                     flags |= DateUtils.FORMAT_24HOUR;
1095                 }
1096             }
1097
1098             String timeRange;
1099             if (showEndTime) {
1100                 timeRange = DateUtils.formatDateRange(mParentActivity,
1101                         event.startMillis, event.endMillis, flags);
1102             } else {
1103                 timeRange = DateUtils.formatDateRange(mParentActivity,
1104                         event.startMillis, event.startMillis, flags);
1105             }
1106
1107             TextView timeView = null;
1108             TextView titleView = null;
1109             switch (eventIndex) {
1110                 case 0:
1111                     timeView = (TextView) mPopupView.findViewById(R.id.time0);
1112                     titleView = (TextView) mPopupView.findViewById(R.id.event_title0);
1113                     break;
1114                 case 1:
1115                     timeView = (TextView) mPopupView.findViewById(R.id.time1);
1116                     titleView = (TextView) mPopupView.findViewById(R.id.event_title1);
1117                     break;
1118                 case 2:
1119                     timeView = (TextView) mPopupView.findViewById(R.id.time2);
1120                     titleView = (TextView) mPopupView.findViewById(R.id.event_title2);
1121                     break;
1122                 case 3:
1123                     timeView = (TextView) mPopupView.findViewById(R.id.time3);
1124                     titleView = (TextView) mPopupView.findViewById(R.id.event_title3);
1125                     break;
1126             }
1127
1128             timeView.setText(timeRange);
1129             titleView.setText(event.title);
1130             eventIndex += 1;
1131         }
1132         if (eventIndex == 0) {
1133             // We didn't find any events for this day
1134             mPopup.dismiss();
1135             return;
1136         }
1137
1138         // Hide the items that have no event information
1139         View view;
1140         switch (eventIndex) {
1141             case 1:
1142                 view = mPopupView.findViewById(R.id.item_layout1);
1143                 view.setVisibility(View.GONE);
1144                 view = mPopupView.findViewById(R.id.item_layout2);
1145                 view.setVisibility(View.GONE);
1146                 view = mPopupView.findViewById(R.id.item_layout3);
1147                 view.setVisibility(View.GONE);
1148                 view = mPopupView.findViewById(R.id.plus_more);
1149                 view.setVisibility(View.GONE);
1150                 break;
1151             case 2:
1152                 view = mPopupView.findViewById(R.id.item_layout1);
1153                 view.setVisibility(View.VISIBLE);
1154                 view = mPopupView.findViewById(R.id.item_layout2);
1155                 view.setVisibility(View.GONE);
1156                 view = mPopupView.findViewById(R.id.item_layout3);
1157                 view.setVisibility(View.GONE);
1158                 view = mPopupView.findViewById(R.id.plus_more);
1159                 view.setVisibility(View.GONE);
1160                 break;
1161             case 3:
1162                 view = mPopupView.findViewById(R.id.item_layout1);
1163                 view.setVisibility(View.VISIBLE);
1164                 view = mPopupView.findViewById(R.id.item_layout2);
1165                 view.setVisibility(View.VISIBLE);
1166                 view = mPopupView.findViewById(R.id.item_layout3);
1167                 view.setVisibility(View.GONE);
1168                 view = mPopupView.findViewById(R.id.plus_more);
1169                 view.setVisibility(View.GONE);
1170                 break;
1171             case 4:
1172                 view = mPopupView.findViewById(R.id.item_layout1);
1173                 view.setVisibility(View.VISIBLE);
1174                 view = mPopupView.findViewById(R.id.item_layout2);
1175                 view.setVisibility(View.VISIBLE);
1176                 view = mPopupView.findViewById(R.id.item_layout3);
1177                 view.setVisibility(View.VISIBLE);
1178                 view = mPopupView.findViewById(R.id.plus_more);
1179                 view.setVisibility(View.GONE);
1180                 break;
1181             default:
1182                 view = mPopupView.findViewById(R.id.item_layout1);
1183                 view.setVisibility(View.VISIBLE);
1184                 view = mPopupView.findViewById(R.id.item_layout2);
1185                 view.setVisibility(View.VISIBLE);
1186                 view = mPopupView.findViewById(R.id.item_layout3);
1187                 view.setVisibility(View.VISIBLE);
1188                 TextView tv = (TextView) mPopupView.findViewById(R.id.plus_more);
1189                 tv.setVisibility(View.VISIBLE);
1190                 String format = mResources.getString(R.string.plus_N_more);
1191                 String plusMore = String.format(format, eventIndex - 4);
1192                 tv.setText(plusMore);
1193                 break;
1194         }
1195
1196         if (eventIndex > 5) {
1197             eventIndex = 5;
1198         }
1199         int popupHeight = 20 * eventIndex + 15;
1200         mPopup.setHeight(popupHeight);
1201
1202         if (mPreviousPopupHeight != popupHeight) {
1203             mPreviousPopupHeight = popupHeight;
1204             mPopup.dismiss();
1205         }
1206         mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, 0, 0);
1207         postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
1208     }
1209
1210     @Override
1211     public boolean onKeyUp(int keyCode, KeyEvent event) {
1212         long duration = event.getEventTime() - event.getDownTime();
1213
1214         switch (keyCode) {
1215         case KeyEvent.KEYCODE_DPAD_CENTER:
1216             if (mSelectionMode == SELECTION_HIDDEN) {
1217                 // Don't do anything unless the selection is visible.
1218                 break;
1219             }
1220
1221             if (mSelectionMode == SELECTION_PRESSED) {
1222                 // This was the first press when there was nothing selected.
1223                 // Change the selection from the "pressed" state to the
1224                 // the "selected" state.  We treat short-press and
1225                 // long-press the same here because nothing was selected.
1226                 mSelectionMode = SELECTION_SELECTED;
1227                 mRedrawScreen = true;
1228                 invalidate();
1229                 break;
1230             }
1231
1232             // Check the duration to determine if this was a short press
1233             if (duration < ViewConfiguration.getLongPressTimeout()) {
1234                 long millis = getSelectedTimeInMillis();
1235                 Utils.startActivity(getContext(), mDetailedView, millis);
1236                 mParentActivity.finish();
1237             } else {
1238                 mSelectionMode = SELECTION_LONGPRESS;
1239                 mRedrawScreen = true;
1240                 invalidate();
1241                 performLongClick();
1242             }
1243         }
1244         return super.onKeyUp(keyCode, event);
1245     }
1246
1247     @Override
1248     public boolean onKeyDown(int keyCode, KeyEvent event) {
1249         if (mSelectionMode == SELECTION_HIDDEN) {
1250             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1251                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
1252                     || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
1253                 // Display the selection box but don't move or select it
1254                 // on this key press.
1255                 mSelectionMode = SELECTION_SELECTED;
1256                 mRedrawScreen = true;
1257                 invalidate();
1258                 return true;
1259             } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
1260                 // Display the selection box but don't select it
1261                 // on this key press.
1262                 mSelectionMode = SELECTION_PRESSED;
1263                 mRedrawScreen = true;
1264                 invalidate();
1265                 return true;
1266             }
1267         }
1268
1269         mSelectionMode = SELECTION_SELECTED;
1270         boolean redraw = false;
1271         Time other = null;
1272
1273         switch (keyCode) {
1274         case KeyEvent.KEYCODE_ENTER:
1275             long millis = getSelectedTimeInMillis();
1276             Utils.startActivity(getContext(), mDetailedView, millis);
1277             mParentActivity.finish();
1278             return true;
1279         case KeyEvent.KEYCODE_DPAD_UP:
1280             if (mCursor.up()) {
1281                 other = mOtherViewCalendar;
1282                 other.set(mViewCalendar);
1283                 other.month -= 1;
1284                 other.monthDay = mCursor.getSelectedDayOfMonth();
1285
1286                 // restore the calendar cursor for the animation
1287                 mCursor.down();
1288             }
1289             redraw = true;
1290             break;
1291
1292         case KeyEvent.KEYCODE_DPAD_DOWN:
1293             if (mCursor.down()) {
1294                 other = mOtherViewCalendar;
1295                 other.set(mViewCalendar);
1296                 other.month += 1;
1297                 other.monthDay = mCursor.getSelectedDayOfMonth();
1298
1299                 // restore the calendar cursor for the animation
1300                 mCursor.up();
1301             }
1302             redraw = true;
1303             break;
1304
1305         case KeyEvent.KEYCODE_DPAD_LEFT:
1306             if (mCursor.left()) {
1307                 other = mOtherViewCalendar;
1308                 other.set(mViewCalendar);
1309                 other.month -= 1;
1310                 other.monthDay = mCursor.getSelectedDayOfMonth();
1311
1312                 // restore the calendar cursor for the animation
1313                 mCursor.right();
1314             }
1315             redraw = true;
1316             break;
1317
1318         case KeyEvent.KEYCODE_DPAD_RIGHT:
1319             if (mCursor.right()) {
1320                 other = mOtherViewCalendar;
1321                 other.set(mViewCalendar);
1322                 other.month += 1;
1323                 other.monthDay = mCursor.getSelectedDayOfMonth();
1324
1325                 // restore the calendar cursor for the animation
1326                 mCursor.left();
1327             }
1328             redraw = true;
1329             break;
1330         }
1331
1332         if (other != null) {
1333             other.normalize(true /* ignore DST */);
1334             mNavigator.goTo(other);
1335         } else if (redraw) {
1336             mRedrawScreen = true;
1337             invalidate();
1338         }
1339
1340         return redraw;
1341     }
1342
1343     class DismissPopup implements Runnable {
1344         public void run() {
1345             mPopup.dismiss();
1346         }
1347     }
1348
1349     // This is called when the activity is paused so that the popup can
1350     // be dismissed.
1351     void dismissPopup() {
1352         if (!mShowToast) {
1353             return;
1354         }
1355
1356         // Protect against null-pointer exceptions
1357         if (mPopup != null) {
1358             mPopup.dismiss();
1359         }
1360
1361         Handler handler = getHandler();
1362         if (handler != null) {
1363             handler.removeCallbacks(mDismissPopup);
1364         }
1365     }
1366 }