blob: e5bb33a748614bd2aa51cdb8a9e575c9fee262c6 [file] [log] [blame]
/*
* 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 float mScale = 0; // Used for supporting different screen densities
private static int WEEK_GAP = 0;
private static int MONTH_DAY_GAP = 1;
private static float HOUR_GAP = 0.5f;
private static int MONTH_DAY_TEXT_SIZE = 20;
private static int WEEK_BANNER_HEIGHT = 17;
private static int WEEK_TEXT_SIZE = 15;
private static int WEEK_TEXT_PADDING = 3;
private static int BUSYBIT_WIDTH = 10;
private static int BUSYBIT_RIGHT_MARGIN = 3;
private static int BUSYBIT_TOP_BOTTOM_MARGIN = 7;
private static 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);
if (mScale == 0) {
mScale = getContext().getResources().getDisplayMetrics().density;
if (mScale != 1) {
WEEK_GAP *= mScale;
MONTH_DAY_GAP *= mScale;
HOUR_GAP *= mScale;
MONTH_DAY_TEXT_SIZE *= mScale;
WEEK_BANNER_HEIGHT *= mScale;
WEEK_TEXT_SIZE *= mScale;
WEEK_TEXT_PADDING *= mScale;
BUSYBIT_WIDTH *= mScale;
BUSYBIT_RIGHT_MARGIN *= mScale;
BUSYBIT_TOP_BOTTOM_MARGIN *= mScale;
HORIZONTAL_FLING_THRESHOLD *= mScale;
}
}
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, true);
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);
}
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();
Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
break;
}
case MenuHelper.MENU_AGENDA: {
long startMillis = getSelectedTimeInMillis();
Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
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) {
// 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);
} 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);
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, true);
} 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);
}
}
}