diff options
Diffstat (limited to 'src/com/android/calendar/MonthView.java')
-rw-r--r-- | src/com/android/calendar/MonthView.java | 1351 |
1 files changed, 1351 insertions, 0 deletions
diff --git a/src/com/android/calendar/MonthView.java b/src/com/android/calendar/MonthView.java new file mode 100644 index 00000000..5949d956 --- /dev/null +++ b/src/com/android/calendar/MonthView.java @@ -0,0 +1,1351 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.calendar; + +import static android.provider.Calendar.EVENT_BEGIN_TIME; +import static android.provider.Calendar.EVENT_END_TIME; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.SystemClock; +import android.pim.DateFormat; +import android.pim.DateUtils; +import android.pim.Time; +import android.provider.Calendar.BusyBits; +import android.util.DayOfMonthCursor; +import android.util.Log; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Calendar; + +public class MonthView extends View implements View.OnCreateContextMenuListener { + + private static final boolean PROFILE_LOAD_TIME = false; + private static final boolean DEBUG_BUSYBITS = false; + + private static final int WEEK_GAP = 1; + private static final int MONTH_DAY_GAP = 1; + private static final float HOUR_GAP = 0.5f; + + private static final int MONTH_DAY_TEXT_SIZE = 20; + private static final int WEEK_BANNER_HEIGHT = 17; + private static final int WEEK_TEXT_SIZE = 15; + private static final int WEEK_TEXT_PADDING = 3; + private static final int BUSYBIT_WIDTH = 10; + private static final int BUSYBIT_RIGHT_MARGIN = 3; + private static final int BUSYBIT_TOP_BOTTOM_MARGIN = 7; + + private static final int HORIZONTAL_FLING_THRESHOLD = 50; + + private int mCellHeight; + private int mBorder; + private boolean mLaunchDayView; + + private GestureDetector mGestureDetector; + + private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW; + + private Time mToday; + private Time mViewCalendar; + private Time mSavedTime = new Time(); // the time when we entered this view + + // This Time object is used to set the time for the other Month view. + private Time mOtherViewCalendar = new Time(); + + // This Time object is used for temporary calculations and is allocated + // once to avoid extra garbage collection + private Time mTempTime = new Time(); + + private DayOfMonthCursor mCursor; + + private Drawable mBoxSelected; + private Drawable mBoxPressed; + private Drawable mBoxLongPressed; + private Drawable mDnaEmpty; + private Drawable mDnaTop; + private Drawable mDnaMiddle; + private Drawable mDnaBottom; + private int mCellWidth; + + private Resources mResources; + private MonthActivity mParentActivity; + private Navigator mNavigator; + private final EventGeometry mEventGeometry; + + // Pre-allocate and reuse + private Rect mRect = new Rect(); + + // The number of hours represented by one busy bit + private static final int HOURS_PER_BUSY_SLOT = 4; + + // The number of database intervals represented by one busy bit (slot) + private static final int INTERVALS_PER_BUSY_SLOT = 4 * 60 / BusyBits.MINUTES_PER_BUSY_INTERVAL; + + // The bit mask for coalescing the raw busy bits from the database + // (1 bit per hour) into the busy bits per slot (4-hour slots). + private static final int BUSY_SLOT_MASK = (1 << INTERVALS_PER_BUSY_SLOT) - 1; + + // The number of slots in a day + private static final int SLOTS_PER_DAY = 24 / HOURS_PER_BUSY_SLOT; + + // There is one "busy" bit for each slot of time. + private byte[][] mBusyBits = new byte[31][SLOTS_PER_DAY]; + + // Raw busy bits from database + private int[] mRawBusyBits = new int[31]; + private int[] mAllDayCounts = new int[31]; + + private PopupWindow mPopup; + private View mPopupView; + private static final int POPUP_HEIGHT = 100; + private int mPreviousPopupHeight; + private static final int POPUP_DISMISS_DELAY = 3000; + private DismissPopup mDismissPopup = new DismissPopup(); + + // For drawing to an off-screen Canvas + private Bitmap mBitmap; + private Canvas mCanvas; + private boolean mRedrawScreen = true; + private Rect mBitmapRect = new Rect(); + private boolean mAnimating; + + // These booleans disable features that were taken out of the spec. + private boolean mShowWeekNumbers = false; + private boolean mShowToast = false; + + // Bitmap caches. + // These improve performance by minimizing calls to NinePatchDrawable.draw() for common + // drawables for events and day backgrounds. + // mEventBitmapCache is indexed by an integer constructed from the bits in the busyBits + // field. It is not expected to be larger than 12 bits (if so, we should switch to using a Map). + // mDayBitmapCache is indexed by a unique integer constructed from the width/height. + private SparseArray<Bitmap> mEventBitmapCache = new SparseArray<Bitmap>(1<<SLOTS_PER_DAY); + private SparseArray<Bitmap> mDayBitmapCache = new SparseArray<Bitmap>(4); + + private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); + + /** + * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. + */ + private static final int SELECTION_HIDDEN = 0; + private static final int SELECTION_PRESSED = 1; + private static final int SELECTION_SELECTED = 2; + private static final int SELECTION_LONGPRESS = 3; + + // Modulo used to pack (width,height) into a unique integer + private static final int MODULO_SHIFT = 16; + + private int mSelectionMode = SELECTION_HIDDEN; + + /** + * The first Julian day of the current month. + */ + private int mFirstJulianDay; + + private final EventLoader mEventLoader; + + private ArrayList<Event> mEvents = new ArrayList<Event>(); + + private Drawable mTodayBackground; + private Drawable mDayBackground; + + // Cached colors + private int mMonthOtherMonthColor; + private int mMonthWeekBannerColor; + private int mMonthOtherMonthBannerColor; + private int mMonthOtherMonthDayNumberColor; + private int mMonthDayNumberColor; + private int mMonthTodayNumberColor; + + public MonthView(MonthActivity activity, Navigator navigator) { + super(activity); + mEventLoader = activity.mEventLoader; + mNavigator = navigator; + mEventGeometry = new EventGeometry(); + mEventGeometry.setMinEventHeight(1.0f); + mEventGeometry.setHourGap(HOUR_GAP); + init(activity); + } + + private void init(MonthActivity activity) { + setFocusable(true); + setClickable(true); + setOnCreateContextMenuListener(this); + mParentActivity = activity; + mViewCalendar = new Time(); + long now = System.currentTimeMillis(); + mViewCalendar.set(now); + mViewCalendar.monthDay = 1; + long millis = mViewCalendar.normalize(true /* ignore DST */); + mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff); + mViewCalendar.set(now); + + mCursor = new DayOfMonthCursor(mViewCalendar.year, mViewCalendar.month, + mViewCalendar.monthDay, mParentActivity.getStartDay()); + mToday = new Time(); + mToday.set(System.currentTimeMillis()); + + mResources = activity.getResources(); + mBoxSelected = mResources.getDrawable(R.drawable.month_view_selected); + mBoxPressed = mResources.getDrawable(R.drawable.month_view_pressed); + mBoxLongPressed = mResources.getDrawable(R.drawable.month_view_longpress); + + mDnaEmpty = mResources.getDrawable(R.drawable.dna_empty); + mDnaTop = mResources.getDrawable(R.drawable.dna_1_of_6); + mDnaMiddle = mResources.getDrawable(R.drawable.dna_2345_of_6); + mDnaBottom = mResources.getDrawable(R.drawable.dna_6_of_6); + mTodayBackground = mResources.getDrawable(R.drawable.month_view_today_background); + mDayBackground = mResources.getDrawable(R.drawable.month_view_background); + + // Cache color lookups + Resources res = getResources(); + mMonthOtherMonthColor = res.getColor(R.color.month_other_month); + mMonthWeekBannerColor = res.getColor(R.color.month_week_banner); + mMonthOtherMonthBannerColor = res.getColor(R.color.month_other_month_banner); + mMonthOtherMonthDayNumberColor = res.getColor(R.color.month_other_month_day_number); + mMonthDayNumberColor = res.getColor(R.color.month_day_number); + mMonthTodayNumberColor = res.getColor(R.color.month_today_number); + + if (mShowToast) { + LayoutInflater inflater; + inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPopupView = inflater.inflate(R.layout.month_bubble, null); + mPopup = new PopupWindow(activity); + mPopup.setContentView(mPopupView); + Resources.Theme dialogTheme = getResources().newTheme(); + dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); + TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { + android.R.attr.windowBackground }); + mPopup.setBackgroundDrawable(ta.getDrawable(0)); + ta.recycle(); + } + + mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + // The user might do a slow "fling" after touching the screen + // and we don't want the long-press to pop up a context menu. + // Setting mLaunchDayView to false prevents the long-press. + mLaunchDayView = false; + mSelectionMode = SELECTION_HIDDEN; + + int distanceX = Math.abs((int) e2.getX() - (int) e1.getX()); + int distanceY = Math.abs((int) e2.getY() - (int) e1.getY()); + if (distanceY < HORIZONTAL_FLING_THRESHOLD || distanceY < distanceX) { + return false; + } + + // Switch to a different month + Time time = mOtherViewCalendar; + time.set(mViewCalendar); + if (velocityY < 0) { + time.month += 1; + } else { + time.month -= 1; + } + time.normalize(true); + mParentActivity.goTo(time); + + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + mLaunchDayView = false; + return true; + } + + @Override + public void onShowPress(MotionEvent e) { + int x = (int) e.getX(); + int y = (int) e.getY(); + int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight); + int col = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth); + if (row > 5) { + row = 5; + } + if (col > 6) { + col = 6; + } + + // Launch the Day/Agenda view when the finger lifts up, + // unless the finger moves before lifting up. + mLaunchDayView = true; + + // Highlight the selected day. + mCursor.setSelectedRowColumn(row, col); + mSelectionMode = SELECTION_PRESSED; + mRedrawScreen = true; + invalidate(); + } + + @Override + public void onLongPress(MotionEvent e) { + // If mLaunchDayView is true, then we haven't done any scrolling + // after touching the screen, so allow long-press to proceed + // with popping up the context menu. + if (mLaunchDayView) { + mLaunchDayView = false; + mSelectionMode = SELECTION_LONGPRESS; + mRedrawScreen = true; + invalidate(); + performLongClick(); + } + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + // If the user moves his finger after touching, then do not + // launch the Day view when he lifts his finger. Also, turn + // off the selection. + mLaunchDayView = false; + + if (mSelectionMode != SELECTION_HIDDEN) { + mSelectionMode = SELECTION_HIDDEN; + mRedrawScreen = true; + invalidate(); + } + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mLaunchDayView) { + mSelectionMode = SELECTION_SELECTED; + mRedrawScreen = true; + invalidate(); + mLaunchDayView = false; + int x = (int) e.getX(); + int y = (int) e.getY(); + long millis = getSelectedMillisFor(x, y); + Utils.startActivity(getContext(), mDetailedView, millis); + mParentActivity.finish(); + } + + return true; + } + }); + } + + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { + MenuItem item; + + item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.day_view); + item.setOnMenuItemClickListener(mContextMenuHandler); + item.setIcon(android.R.drawable.ic_menu_day); + item.setAlphabeticShortcut('d'); + + item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.agenda_view); + item.setOnMenuItemClickListener(mContextMenuHandler); + item.setIcon(android.R.drawable.ic_menu_agenda); + item.setAlphabeticShortcut('a'); + + item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create); + item.setOnMenuItemClickListener(mContextMenuHandler); + item.setIcon(android.R.drawable.ic_menu_add); + item.setAlphabeticShortcut('n'); + } + + private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case MenuHelper.MENU_DAY: { + long startMillis = getSelectedTimeInMillis(); + MenuHelper.switchTo(mParentActivity, DayActivity.class.getName(), startMillis); + mParentActivity.finish(); + break; + } + case MenuHelper.MENU_AGENDA: { + long startMillis = getSelectedTimeInMillis(); + MenuHelper.switchTo(mParentActivity, AgendaActivity.class.getName(), startMillis); + mParentActivity.finish(); + break; + } + case MenuHelper.MENU_EVENT_CREATE: { + long startMillis = getSelectedTimeInMillis(); + long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setClassName(mContext, EditEvent.class.getName()); + intent.putExtra(EVENT_BEGIN_TIME, startMillis); + intent.putExtra(EVENT_END_TIME, endMillis); + mParentActivity.startActivity(intent); + break; + } + default: { + return false; + } + } + return true; + } + } + + void reloadEvents() { + // Get the date for the beginning of the month + Time monthStart = mTempTime; + monthStart.set(mViewCalendar); + monthStart.monthDay = 1; + monthStart.hour = 0; + monthStart.minute = 0; + monthStart.second = 0; + long millis = monthStart.normalize(true /* ignore isDst */); + int startDay = Time.getJulianDay(millis, monthStart.gmtoff); + + // Load the busy-bits in the background + mParentActivity.startProgressSpinner(); + final long startMillis; + if (PROFILE_LOAD_TIME) { + startMillis = SystemClock.uptimeMillis(); + } else { + // To avoid a compiler error that this variable might not be initialized. + startMillis = 0; + } + mEventLoader.loadBusyBitsInBackground(startDay, 31, mRawBusyBits, mAllDayCounts, + new Runnable() { + public void run() { + convertBusyBits(); + if (PROFILE_LOAD_TIME) { + long endMillis = SystemClock.uptimeMillis(); + long elapsed = endMillis - startMillis; + Log.i("Cal", (mViewCalendar.month+1) + "/" + mViewCalendar.year + " Month view load busybits: " + elapsed); + } + mRedrawScreen = true; + mParentActivity.stopProgressSpinner(); + invalidate(); + } + }); + } + + void animationStarted() { + mAnimating = true; + } + + void animationFinished() { + mAnimating = false; + mRedrawScreen = true; + invalidate(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + drawingCalc(width, height); + // If the size changed, then we should rebuild the bitmaps... + clearBitmapCache(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // No need to hang onto the bitmaps... + clearBitmapCache(); + if (mBitmap != null) { + mBitmap.recycle(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mRedrawScreen) { + if (mCanvas == null) { + drawingCalc(getWidth(), getHeight()); + } + + // If we are zero-sized, the canvas will remain null so check again + if (mCanvas != null) { + // Clear the background + final Canvas bitmapCanvas = mCanvas; + bitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + doDraw(bitmapCanvas); + mRedrawScreen = false; + } + } + + // If we are zero-sized, the bitmap will be null so guard against this + if (mBitmap != null) { + canvas.drawBitmap(mBitmap, mBitmapRect, mBitmapRect, null); + } + } + + private void doDraw(Canvas canvas) { + boolean isLandscape = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + + Paint p = new Paint(); + Rect r = mRect; + int columnDay1 = mCursor.getColumnOf(1); + + // Get the Julian day for the date at row 0, column 0. + int day = mFirstJulianDay - columnDay1; + + int weekNum = 0; + Calendar calendar = null; + if (mShowWeekNumbers) { + calendar = Calendar.getInstance(); + boolean noPrevMonth = (columnDay1 == 0); + + // Compute the week number for the first row. + weekNum = getWeekOfYear(0, 0, noPrevMonth, calendar); + } + + for (int row = 0; row < 6; row++) { + for (int column = 0; column < 7; column++) { + drawBox(day, weekNum, row, column, canvas, p, r, isLandscape); + day += 1; + } + + if (mShowWeekNumbers) { + weekNum += 1; + if (weekNum >= 53) { + boolean inCurrentMonth = (day - mFirstJulianDay < 31); + weekNum = getWeekOfYear(row + 1, 0, inCurrentMonth, calendar); + } + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) { + return true; + } + + return super.onTouchEvent(event); + } + + private long getSelectedMillisFor(int x, int y) { + int row = (y - WEEK_GAP) / (WEEK_GAP + mCellHeight); + int column = (x - mBorder) / (MONTH_DAY_GAP + mCellWidth); + if (column > 6) { + column = 6; + } + + DayOfMonthCursor c = mCursor; + Time time = mTempTime; + time.set(mViewCalendar); + + // Compute the day number from the row and column. If the row and + // column are in a different month from the current one, then the + // monthDay might be negative or it might be greater than the number + // of days in this month, but that is okay because the normalize() + // method will adjust the month (and year) if necessary. + time.monthDay = 7 * row + column - c.getOffset() + 1; + return time.normalize(true); + } + + /** + * Create a bitmap at the origin and draw the drawable to it using the bounds specified by rect. + * + * @param drawable the drawable we wish to render + * @param width the width of the resulting bitmap + * @param height the height of the resulting bitmap + * @return a new bitmap + */ + private Bitmap createBitmap(Drawable drawable, int width, int height) { + // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888) + Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig()); + + // Draw the drawable into the bitmap at the origin. + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + return bitmap; + } + + /** + * Clears the bitmap cache. Generally only needed when the screen size changed. + */ + private void clearBitmapCache() { + recycleAndClearBitmapCache(mEventBitmapCache); + recycleAndClearBitmapCache(mDayBitmapCache); + } + + private void recycleAndClearBitmapCache(SparseArray<Bitmap> bitmapCache) { + int size = bitmapCache.size(); + for(int i = 0; i < size; i++) { + bitmapCache.valueAt(i).recycle(); + } + bitmapCache.clear(); + + } + + /** + * Draw a single box onto the canvas. + * @param day The Julian day. + * @param weekNum The week number. + * @param row The row of the box (0-5). + * @param column The column of the box (0-6). + * @param canvas The canvas to draw on. + * @param p The paint used for drawing. + * @param r The rectangle used for each box. + * @param isLandscape Is the current orientation landscape. + */ + private void drawBox(int day, int weekNum, int row, int column, Canvas canvas, Paint p, + Rect r, boolean isLandscape) { + + // Only draw the selection if we are in the press state or if we have + // moved the cursor with key input. + boolean drawSelection = false; + if (mSelectionMode != SELECTION_HIDDEN) { + drawSelection = mCursor.isSelected(row, column); + } + + boolean withinCurrentMonth = mCursor.isWithinCurrentMonth(row, column); + boolean isToday = false; + int dayOfBox = mCursor.getDayAt(row, column); + if (dayOfBox == mToday.monthDay && mCursor.getYear() == mToday.year + && mCursor.getMonth() == mToday.month) { + isToday = true; + } + + int y = WEEK_GAP + row*(WEEK_GAP + mCellHeight); + int x = mBorder + column*(MONTH_DAY_GAP + mCellWidth); + + r.left = x; + r.top = y; + r.right = x + mCellWidth; + r.bottom = y + mCellHeight; + + + // Adjust the left column, right column, and bottom row to leave + // no border. + if (column == 0) { + r.left = -1; + } else if (column == 6) { + r.right += mBorder + 2; + } + + if (row == 5) { + r.bottom = getMeasuredHeight(); + } + + // Draw the cell contents (excluding monthDay number) + if (!withinCurrentMonth) { + boolean firstDayOfNextmonth = isFirstDayOfNextMonth(row, column); + + // Adjust cell boundaries to compensate for the different border + // style. + r.top--; + if (column != 0) { + r.left--; + } + + // Draw cell border + p.setColor(mMonthOtherMonthColor); + p.setAntiAlias(false); + + if (row == 0) { + // Bottom line + canvas.drawLine(r.left, r.bottom, r.right, r.bottom, p); + } + + // Top line + canvas.drawLine(r.left, r.top, r.right, r.top, p); + + // Right line + canvas.drawLine(r.right, r.top, r.right, r.bottom, p); + + if (firstDayOfNextmonth && column != 0) { + canvas.drawLine(r.left, r.top, r.left, r.bottom, p); + } + } else if (drawSelection) { + if (mSelectionMode == SELECTION_SELECTED) { + mBoxSelected.setBounds(r); + mBoxSelected.draw(canvas); + } else if (mSelectionMode == SELECTION_PRESSED) { + mBoxPressed.setBounds(r); + mBoxPressed.draw(canvas); + } else { + mBoxLongPressed.setBounds(r); + mBoxLongPressed.draw(canvas); + } + + drawEvents(day, canvas, r, p); + if (!mAnimating) { + updateEventDetails(day); + } + } else { + // Today gets a different background + if (isToday) { + // We could cache this for a little bit more performance, but it's not on the + // performance radar... + Drawable background = mTodayBackground; + background.setBounds(r); + background.draw(canvas); + } else { + // Use the bitmap cache to draw the day background + int width = r.right - r.left; + int height = r.bottom - r.top; + // Compute a unique id that depends on width and height. + int id = (height << MODULO_SHIFT) | width; + Bitmap bitmap = mDayBitmapCache.get(id); + if (bitmap == null) { + bitmap = createBitmap(mDayBackground, width, height); + mDayBitmapCache.put(id, bitmap); + } + canvas.drawBitmap(bitmap, r.left, r.top, p); + } + drawEvents(day, canvas, r, p); + } + + // Draw week number + if (mShowWeekNumbers && column == 0) { + // Draw the banner + p.setStyle(Paint.Style.FILL); + p.setColor(mMonthWeekBannerColor); + int right = r.right; + r.right = right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN; + if (isLandscape) { + int bottom = r.bottom; + r.bottom = r.top + WEEK_BANNER_HEIGHT; + r.left++; + canvas.drawRect(r, p); + r.bottom = bottom; + r.left--; + } else { + int top = r.top; + r.top = r.bottom - WEEK_BANNER_HEIGHT; + r.left++; + canvas.drawRect(r, p); + r.top = top; + r.left--; + } + r.right = right; + + // Draw the number + p.setColor(mMonthOtherMonthBannerColor); + p.setAntiAlias(true); + p.setTypeface(null); + p.setTextSize(WEEK_TEXT_SIZE); + p.setTextAlign(Paint.Align.LEFT); + + int textX = r.left + WEEK_TEXT_PADDING; + int textY; + if (isLandscape) { + textY = r.top + WEEK_BANNER_HEIGHT - WEEK_TEXT_PADDING; + } else { + textY = r.bottom - WEEK_TEXT_PADDING; + } + + canvas.drawText(String.valueOf(weekNum), textX, textY, p); + } + + // Draw the monthDay number + p.setStyle(Paint.Style.FILL); + p.setAntiAlias(true); + p.setTypeface(null); + p.setTextSize(MONTH_DAY_TEXT_SIZE); + + if (!withinCurrentMonth) { + p.setColor(mMonthOtherMonthDayNumberColor); + } else if (drawSelection || !isToday) { + p.setColor(mMonthDayNumberColor); + } else { + p.setColor(mMonthTodayNumberColor); + } + + p.setTextAlign(Paint.Align.CENTER); + int right = r.right - BUSYBIT_WIDTH - BUSYBIT_RIGHT_MARGIN; + int textX = r.left + (right - r.left) / 2; // center of text + int textY = r.bottom - BUSYBIT_TOP_BOTTOM_MARGIN - 2; // bottom of text + canvas.drawText(String.valueOf(mCursor.getDayAt(row, column)), textX, textY, p); + } + + /** + * Converts the busy bits from the database that use 1-hour intervals to + * the 4-hour time slots needed in this view. Also, we map all-day + * events to the first two 4-hour time slots (that is, an all-day event + * will look like the first 8 hours from 12am to 8am are busy). This + * looks better than setting just the first 4-hour time slot because that + * is barely visible in landscape mode. + */ + private void convertBusyBits() { + if (DEBUG_BUSYBITS) { + Log.i("Cal", "convertBusyBits() SLOTS_PER_DAY: " + SLOTS_PER_DAY + + " BUSY_SLOT_MASK: " + BUSY_SLOT_MASK + + " INTERVALS_PER_BUSY_SLOT: " + INTERVALS_PER_BUSY_SLOT); + for (int day = 0; day < 31; day++) { + int bits = mRawBusyBits[day]; + String bitString = String.format("0x%06x", bits); + String valString = ""; + for (int slot = 0; slot < SLOTS_PER_DAY; slot++) { + int val = bits & BUSY_SLOT_MASK; + bits = bits >>> INTERVALS_PER_BUSY_SLOT; + valString += " " + val; + } + Log.i("Cal", "[" + day + "] " + bitString + " " + valString + + " allday: " + mAllDayCounts[day]); + } + } + for (int day = 0; day < 31; day++) { + int bits = mRawBusyBits[day]; + for (int slot = 0; slot < SLOTS_PER_DAY; slot++) { + int val = bits & BUSY_SLOT_MASK; + bits = bits >>> INTERVALS_PER_BUSY_SLOT; + if (val == 0) { + mBusyBits[day][slot] = 0; + } else { + mBusyBits[day][slot] = 1; + } + } + if (mAllDayCounts[day] > 0) { + mBusyBits[day][0] = 1; + mBusyBits[day][1] = 1; + } + } + } + + /** + * Create a bitmap at the origin for the given set of busyBits. + * + * @param busyBits an array of bits with elements set to 1 if we have an event for that slot + * @param rect the size of the resulting + * @return a new bitmap + */ + private Bitmap createEventBitmap(byte[] busyBits, Rect rect) { + // Compute the size of the smallest bitmap, excluding margins. + final int left = 0; + final int right = BUSYBIT_WIDTH; + final int top = 0; + final int bottom = (rect.bottom - rect.top) - 2 * BUSYBIT_TOP_BOTTOM_MARGIN; + final int height = bottom - top; + final int width = right - left; + + final Drawable dnaEmpty = mDnaEmpty; + final Drawable dnaTop = mDnaTop; + final Drawable dnaMiddle = mDnaMiddle; + final Drawable dnaBottom = mDnaBottom; + final float slotHeight = (float) height / SLOTS_PER_DAY; + + // Create a bitmap with the same format as mBitmap (should be Bitmap.Config.ARGB_8888) + Bitmap bitmap = Bitmap.createBitmap(width, height, mBitmap.getConfig()); + + // Create a canvas for drawing and draw background (dnaEmpty) + Canvas canvas = new Canvas(bitmap); + dnaEmpty.setBounds(left, top, right, bottom); + dnaEmpty.draw(canvas); + + // The first busy bit is a drawable that is round at the top + if (busyBits[0] == 1) { + float rectBottom = top + slotHeight; + dnaTop.setBounds(left, top, right, (int) rectBottom); + dnaTop.draw(canvas); + } + + // The last busy bit is a drawable that is round on the bottom + int lastIndex = busyBits.length - 1; + if (busyBits[lastIndex] == 1) { + float rectTop = bottom - slotHeight; + dnaBottom.setBounds(left, (int) rectTop, right, bottom); + dnaBottom.draw(canvas); + } + + // Draw all intermediate pieces. We could further optimize this to + // draw runs of bits, but it probably won't yield much more performance. + float rectTop = top + slotHeight; + for (int index = 1; index < lastIndex; index++) { + float rectBottom = rectTop + slotHeight; + if (busyBits[index] == 1) { + dnaMiddle.setBounds(left, (int) rectTop, right, (int) rectBottom); + dnaMiddle.draw(canvas); + } + rectTop = rectBottom; + } + return bitmap; + } + + private void drawEvents(int date, Canvas canvas, Rect rect, Paint p) { + // These are the coordinates of the upper left corner where we'll draw the event bitmap + int top = rect.top + BUSYBIT_TOP_BOTTOM_MARGIN; + int right = rect.right - BUSYBIT_RIGHT_MARGIN; + int left = right - BUSYBIT_WIDTH; + + // Display the busy bits. Draw a rectangle for each run of 1-bits. + int day = date - mFirstJulianDay; + byte[] busyBits = mBusyBits[day]; + int lastIndex = busyBits.length - 1; + + // Cache index is simply all of the bits combined into an integer + int cacheIndex = 0; + for (int i = 0 ; i <= lastIndex; i++) cacheIndex |= busyBits[i] << i; + Bitmap bitmap = mEventBitmapCache.get(cacheIndex); + if (bitmap == null) { + // Create a bitmap that we'll reuse for all events with the same + // combination of busyBits. + bitmap = createEventBitmap(busyBits, rect); + mEventBitmapCache.put(cacheIndex, bitmap); + } + canvas.drawBitmap(bitmap, left, top, p); + } + + private boolean isFirstDayOfNextMonth(int row, int column) { + if (column == 0) { + column = 6; + row--; + } else { + column--; + } + return mCursor.isWithinCurrentMonth(row, column); + } + + private int getWeekOfYear(int row, int column, boolean isWithinCurrentMonth, + Calendar calendar) { + calendar.set(Calendar.DAY_OF_MONTH, mCursor.getDayAt(row, column)); + if (isWithinCurrentMonth) { + calendar.set(Calendar.MONTH, mCursor.getMonth()); + calendar.set(Calendar.YEAR, mCursor.getYear()); + } else { + int month = mCursor.getMonth(); + int year = mCursor.getYear(); + if (row < 2) { + // Previous month + if (month == 0) { + year--; + month = 11; + } else { + month--; + } + } else { + // Next month + if (month == 11) { + year++; + month = 0; + } else { + month++; + } + } + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.YEAR, year); + } + + return calendar.get(Calendar.WEEK_OF_YEAR); + } + + void setDetailedView(String detailedView) { + mDetailedView = detailedView; + } + + void setSelectedTime(Time time) { + // Save the selected time so that we can restore it later when we switch views. + mSavedTime.set(time); + + mViewCalendar.set(time); + mViewCalendar.monthDay = 1; + long millis = mViewCalendar.normalize(true /* ignore DST */); + mFirstJulianDay = Time.getJulianDay(millis, mViewCalendar.gmtoff); + mViewCalendar.set(time); + + mCursor = new DayOfMonthCursor(time.year, time.month, time.monthDay, + mCursor.getWeekStartDay()); + + mRedrawScreen = true; + invalidate(); + } + + public long getSelectedTimeInMillis() { + Time time = mTempTime; + time.set(mViewCalendar); + + time.monthDay = mCursor.getSelectedDayOfMonth(); + + // Restore the saved hour:minute:second offset from when we entered + // this view. + time.second = mSavedTime.second; + time.minute = mSavedTime.minute; + time.hour = mSavedTime.hour; + return time.normalize(true); + } + + Time getTime() { + return mViewCalendar; + } + + public int getSelectionMode() { + return mSelectionMode; + } + + public void setSelectionMode(int selectionMode) { + mSelectionMode = selectionMode; + } + + private void drawingCalc(int width, int height) { + mCellHeight = (height - (6 * WEEK_GAP)) / 6; + mEventGeometry.setHourHeight((mCellHeight - 25.0f * HOUR_GAP) / 24.0f); + mCellWidth = (width - (6 * MONTH_DAY_GAP)) / 7; + mBorder = (width - 6 * (mCellWidth + MONTH_DAY_GAP) - mCellWidth) / 2; + + if (mShowToast) { + mPopup.dismiss(); + mPopup.setWidth(width - 20); + mPopup.setHeight(POPUP_HEIGHT); + } + + if (((mBitmap == null) + || mBitmap.isRecycled() + || (mBitmap.getHeight() != height) + || (mBitmap.getWidth() != width)) + && (width > 0) && (height > 0)) { + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + } + + mBitmapRect.top = 0; + mBitmapRect.bottom = height; + mBitmapRect.left = 0; + mBitmapRect.right = width; + } + + private void updateEventDetails(int date) { + if (!mShowToast) { + return; + } + + getHandler().removeCallbacks(mDismissPopup); + ArrayList<Event> events = mEvents; + int numEvents = events.size(); + if (numEvents == 0) { + mPopup.dismiss(); + return; + } + + int eventIndex = 0; + for (int i = 0; i < numEvents; i++) { + Event event = events.get(i); + + if (event.startDay > date || event.endDay < date) { + continue; + } + + // If we have all the event that we can display, then just count + // the extra ones. + if (eventIndex >= 4) { + eventIndex += 1; + continue; + } + + int flags; + boolean showEndTime = false; + if (event.allDay) { + int numDays = event.endDay - event.startDay; + if (numDays == 0) { + flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; + } else { + showEndTime = true; + flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_ALL; + } + } else { + flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; + if (DateFormat.is24HourFormat(mContext)) { + flags |= DateUtils.FORMAT_24HOUR; + } + } + + String timeRange; + if (showEndTime) { + timeRange = DateUtils.formatDateRange(event.startMillis, event.endMillis, flags); + } else { + timeRange = DateUtils.formatDateRange(event.startMillis, event.startMillis, flags); + } + + TextView timeView = null; + TextView titleView = null; + switch (eventIndex) { + case 0: + timeView = (TextView) mPopupView.findViewById(R.id.time0); + titleView = (TextView) mPopupView.findViewById(R.id.event_title0); + break; + case 1: + timeView = (TextView) mPopupView.findViewById(R.id.time1); + titleView = (TextView) mPopupView.findViewById(R.id.event_title1); + break; + case 2: + timeView = (TextView) mPopupView.findViewById(R.id.time2); + titleView = (TextView) mPopupView.findViewById(R.id.event_title2); + break; + case 3: + timeView = (TextView) mPopupView.findViewById(R.id.time3); + titleView = (TextView) mPopupView.findViewById(R.id.event_title3); + break; + } + + timeView.setText(timeRange); + titleView.setText(event.title); + eventIndex += 1; + } + if (eventIndex == 0) { + // We didn't find any events for this day + mPopup.dismiss(); + return; + } + + // Hide the items that have no event information + View view; + switch (eventIndex) { + case 1: + view = mPopupView.findViewById(R.id.item_layout1); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.item_layout2); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.item_layout3); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.plus_more); + view.setVisibility(View.GONE); + break; + case 2: + view = mPopupView.findViewById(R.id.item_layout1); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout2); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.item_layout3); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.plus_more); + view.setVisibility(View.GONE); + break; + case 3: + view = mPopupView.findViewById(R.id.item_layout1); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout2); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout3); + view.setVisibility(View.GONE); + view = mPopupView.findViewById(R.id.plus_more); + view.setVisibility(View.GONE); + break; + case 4: + view = mPopupView.findViewById(R.id.item_layout1); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout2); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout3); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.plus_more); + view.setVisibility(View.GONE); + break; + default: + view = mPopupView.findViewById(R.id.item_layout1); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout2); + view.setVisibility(View.VISIBLE); + view = mPopupView.findViewById(R.id.item_layout3); + view.setVisibility(View.VISIBLE); + TextView tv = (TextView) mPopupView.findViewById(R.id.plus_more); + tv.setVisibility(View.VISIBLE); + String format = mResources.getString(R.string.plus_N_more); + String plusMore = String.format(format, eventIndex - 4); + tv.setText(plusMore); + break; + } + + if (eventIndex > 5) { + eventIndex = 5; + } + int popupHeight = 20 * eventIndex + 15; + mPopup.setHeight(popupHeight); + + if (mPreviousPopupHeight != popupHeight) { + mPreviousPopupHeight = popupHeight; + mPopup.dismiss(); + } + mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, 0, 0); + postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + long duration = event.getEventTime() - event.getDownTime(); + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + if (mSelectionMode == SELECTION_HIDDEN) { + // Don't do anything unless the selection is visible. + break; + } + + if (mSelectionMode == SELECTION_PRESSED) { + // This was the first press when there was nothing selected. + // Change the selection from the "pressed" state to the + // the "selected" state. We treat short-press and + // long-press the same here because nothing was selected. + mSelectionMode = SELECTION_SELECTED; + mRedrawScreen = true; + invalidate(); + break; + } + + // Check the duration to determine if this was a short press + if (duration < ViewConfiguration.getLongPressTimeout()) { + long millis = getSelectedTimeInMillis(); + Utils.startActivity(getContext(), mDetailedView, millis); + mParentActivity.finish(); + } else { + mSelectionMode = SELECTION_LONGPRESS; + mRedrawScreen = true; + invalidate(); + performLongClick(); + } + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mSelectionMode == SELECTION_HIDDEN) { + if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // Display the selection box but don't move or select it + // on this key press. + mSelectionMode = SELECTION_SELECTED; + mRedrawScreen = true; + invalidate(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + // Display the selection box but don't select it + // on this key press. + mSelectionMode = SELECTION_PRESSED; + mRedrawScreen = true; + invalidate(); + return true; + } + } + + mSelectionMode = SELECTION_SELECTED; + boolean redraw = false; + Time other = null; + + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + long millis = getSelectedTimeInMillis(); + Utils.startActivity(getContext(), mDetailedView, millis); + mParentActivity.finish(); + return true; + case KeyEvent.KEYCODE_DPAD_UP: + if (mCursor.up()) { + other = mOtherViewCalendar; + other.set(mViewCalendar); + other.month -= 1; + other.monthDay = mCursor.getSelectedDayOfMonth(); + + // restore the calendar cursor for the animation + mCursor.down(); + } + redraw = true; + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mCursor.down()) { + other = mOtherViewCalendar; + other.set(mViewCalendar); + other.month += 1; + other.monthDay = mCursor.getSelectedDayOfMonth(); + + // restore the calendar cursor for the animation + mCursor.up(); + } + redraw = true; + break; + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (mCursor.left()) { + other = mOtherViewCalendar; + other.set(mViewCalendar); + other.month -= 1; + other.monthDay = mCursor.getSelectedDayOfMonth(); + + // restore the calendar cursor for the animation + mCursor.right(); + } + redraw = true; + break; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (mCursor.right()) { + other = mOtherViewCalendar; + other.set(mViewCalendar); + other.month += 1; + other.monthDay = mCursor.getSelectedDayOfMonth(); + + // restore the calendar cursor for the animation + mCursor.left(); + } + redraw = true; + break; + } + + if (other != null) { + other.normalize(true /* ignore DST */); + mNavigator.goTo(other); + } else if (redraw) { + mRedrawScreen = true; + invalidate(); + } + + return redraw; + } + + class DismissPopup implements Runnable { + public void run() { + mPopup.dismiss(); + } + } + + // This is called when the activity is paused so that the popup can + // be dismissed. + void dismissPopup() { + if (!mShowToast) { + return; + } + + // Protect against null-pointer exceptions + if (mPopup != null) { + mPopup.dismiss(); + } + + Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mDismissPopup); + } + } +} |