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