/* * 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.provider.Calendar.BusyBits; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.text.format.Time; 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 = 0; 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(getContext(), 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; final long startMillis = getSelectedTimeInMillis(); final int flags = DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags); menu.setHeaderTitle(title); item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_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.show_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); } } } drawGrid(canvas, p); } @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 the grid lines for the calendar * @param canvas The canvas to draw on. * @param p The paint used for drawing. */ private void drawGrid(Canvas canvas, Paint p) { p.setColor(mMonthOtherMonthColor); p.setAntiAlias(false); final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); for (int row = 0; row < 6; row++) { int y = WEEK_GAP + row * (WEEK_GAP + mCellHeight) - 1; canvas.drawLine(0, y, width, y, p); } for (int column = 1; column < 7; column++) { int x = mBorder + column * (MONTH_DAY_GAP + mCellWidth) - 1; canvas.drawLine(x, WEEK_GAP, x, height, p); } } /** * 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--; } } 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.month += mCursor.getSelectedMonthOffset(); 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(mParentActivity, event.startMillis, event.endMillis, flags); } else { timeRange = DateUtils.formatDateRange(mParentActivity, 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); } } }