1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calendar; 18 19 import static android.provider.Calendar.EVENT_BEGIN_TIME; 20 import static android.provider.Calendar.EVENT_END_TIME; 21 22 import android.content.ContentResolver; 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.Paint; 33 import android.graphics.Paint.Style; 34 import android.graphics.Path; 35 import android.graphics.Path.Direction; 36 import android.graphics.PorterDuff; 37 import android.graphics.Rect; 38 import android.graphics.RectF; 39 import android.graphics.Typeface; 40 import android.net.Uri; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.provider.Calendar.Attendees; 44 import android.provider.Calendar.Calendars; 45 import android.provider.Calendar.Events; 46 import android.text.TextUtils; 47 import android.text.format.DateFormat; 48 import android.text.format.DateUtils; 49 import android.text.format.Time; 50 import android.util.Log; 51 import android.view.ContextMenu; 52 import android.view.ContextMenu.ContextMenuInfo; 53 import android.view.Gravity; 54 import android.view.KeyEvent; 55 import android.view.LayoutInflater; 56 import android.view.MenuItem; 57 import android.view.MotionEvent; 58 import android.view.View; 59 import android.view.ViewConfiguration; 60 import android.view.ViewGroup; 61 import android.view.WindowManager; 62 import android.view.accessibility.AccessibilityEvent; 63 import android.view.accessibility.AccessibilityManager; 64 import android.widget.ImageView; 65 import android.widget.PopupWindow; 66 import android.widget.TextView; 67 68 import java.util.ArrayList; 69 import java.util.Calendar; 70 import java.util.Locale; 71 import java.util.TimeZone; 72 import java.util.regex.Matcher; 73 import java.util.regex.Pattern; 74 75 /** 76 * This is the base class for a set of classes that implement views (day view 77 * and week view to start with) that share some common code. 78 */ 79 public class CalendarView extends View 80 implements View.OnCreateContextMenuListener, View.OnClickListener { 81 82 private static float mScale = 0; // Used for supporting different screen densities 83 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event 84 85 private boolean mOnFlingCalled; 86 /** 87 * ID of the last event which was displayed with the toast popup. 88 * 89 * This is used to prevent popping up multiple quick views for the same event, especially 90 * during calendar syncs. This becomes valid when an event is selected, either by default 91 * on starting calendar or by scrolling to an event. It becomes invalid when the user 92 * explicitly scrolls to an empty time slot, changes views, or deletes the event. 93 */ 94 private long mLastPopupEventID; 95 96 protected CalendarApplication mCalendarApp; 97 protected CalendarActivity mParentActivity; 98 99 // This runs when we need to update the tz 100 private Runnable mUpdateTZ = new Runnable() { 101 @Override 102 public void run() { 103 String tz = Utils.getTimeZone(mContext, this); 104 // BaseDate we want to keep on the same day, so we swap tz 105 mBaseDate.timezone = tz; 106 mBaseDate.normalize(true); 107 // CurrentTime we want to keep at the same absolute time, so we 108 // call switch tz 109 mCurrentTime.switchTimezone(tz); 110 mTimeZone = TimeZone.getTimeZone(tz); 111 recalc(); 112 mTitleTextView.setText(mDateRange); 113 } 114 }; 115 private Context mContext; 116 117 private static final String[] CALENDARS_PROJECTION = new String[] { 118 Calendars._ID, // 0 119 Calendars.ACCESS_LEVEL, // 1 120 Calendars.OWNER_ACCOUNT, // 2 121 }; 122 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; 123 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 124 private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; 125 126 private static final String[] ATTENDEES_PROJECTION = new String[] { 127 Attendees._ID, // 0 128 Attendees.ATTENDEE_RELATIONSHIP, // 1 129 }; 130 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1; 131 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d"; 132 133 private static float SMALL_ROUND_RADIUS = 3.0F; 134 135 private static final int FROM_NONE = 0; 136 private static final int FROM_ABOVE = 1; 137 private static final int FROM_BELOW = 2; 138 private static final int FROM_LEFT = 4; 139 private static final int FROM_RIGHT = 8; 140 141 private static final int ACCESS_LEVEL_NONE = 0; 142 private static final int ACCESS_LEVEL_DELETE = 1; 143 private static final int ACCESS_LEVEL_EDIT = 2; 144 145 private static int HORIZONTAL_SCROLL_THRESHOLD = 50; 146 147 private ContinueScroll mContinueScroll = new ContinueScroll(); 148 149 static private class DayHeader{ 150 int cell; 151 String dateString; 152 } 153 154 private DayHeader[] dayHeaders = new DayHeader[32]; 155 156 // Make this visible within the package for more informative debugging 157 Time mBaseDate; 158 private Time mCurrentTime; 159 //Update the current time line every five minutes if the window is left open that long 160 private static final int UPDATE_CURRENT_TIME_DELAY = 300000; 161 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); 162 private int mTodayJulianDay; 163 164 private Typeface mBold = Typeface.DEFAULT_BOLD; 165 private int mFirstJulianDay; 166 private int mLastJulianDay; 167 168 private int mMonthLength; 169 private int mFirstDate; 170 private int[] mEarliestStartHour; // indexed by the week day offset 171 private boolean[] mHasAllDayEvent; // indexed by the week day offset 172 173 private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW; 174 175 /** 176 * This variable helps to avoid unnecessarily reloading events by keeping 177 * track of the start millis parameter used for the most recent loading 178 * of events. If the next reload matches this, then the events are not 179 * reloaded. To force a reload, set this to zero (this is set to zero 180 * in the method clearCachedEvents()). 181 */ 182 private long mLastReloadMillis; 183 184 private ArrayList<Event> mEvents = new ArrayList<Event>(); 185 private int mSelectionDay; // Julian day 186 private int mSelectionHour; 187 188 /* package private so that CalendarActivity can read it when creating new 189 * events 190 */ 191 boolean mSelectionAllDay; 192 193 private int mCellWidth; 194 195 // Pre-allocate these objects and re-use them 196 private Rect mRect = new Rect(); 197 private RectF mRectF = new RectF(); 198 private Rect mSrcRect = new Rect(); 199 private Rect mDestRect = new Rect(); 200 private Paint mPaint = new Paint(); 201 private Paint mPaintBorder = new Paint(); 202 private Paint mEventTextPaint = new Paint(); 203 private Paint mSelectionPaint = new Paint(); 204 private Path mPath = new Path(); 205 206 protected boolean mDrawTextInEventRect; 207 private int mStartDay; 208 209 private PopupWindow mPopup; 210 private View mPopupView; 211 212 // The number of milliseconds to show the popup window 213 private static final int POPUP_DISMISS_DELAY = 3000; 214 private DismissPopup mDismissPopup = new DismissPopup(); 215 216 // For drawing to an off-screen Canvas 217 private Bitmap mBitmap; 218 private Canvas mCanvas; 219 private boolean mRedrawScreen = true; 220 private boolean mRemeasure = true; 221 222 private final EventLoader mEventLoader; 223 protected final EventGeometry mEventGeometry; 224 225 private static final int DAY_GAP = 1; 226 private static final int HOUR_GAP = 1; 227 private static int SINGLE_ALLDAY_HEIGHT = 20; 228 private static int MAX_ALLDAY_HEIGHT = 72; 229 private static int ALLDAY_TOP_MARGIN = 3; 230 private static int MAX_ALLDAY_EVENT_HEIGHT = 18; 231 232 /* The extra space to leave above the text in all-day events */ 233 private static final int ALL_DAY_TEXT_TOP_MARGIN = 0; 234 235 /* The extra space to leave above the text in normal events */ 236 private static final int NORMAL_TEXT_TOP_MARGIN = 2; 237 238 private static final int HOURS_LEFT_MARGIN = 2; 239 private static final int HOURS_RIGHT_MARGIN = 4; 240 private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 241 242 private static int CURRENT_TIME_LINE_HEIGHT = 2; 243 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1; 244 private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6; 245 private static int CURRENT_TIME_MARKER_HEIGHT = 6; 246 private static int CURRENT_TIME_MARKER_WIDTH = 8; 247 private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1; 248 249 /* package */ static final int MINUTES_PER_HOUR = 60; 250 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; 251 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; 252 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); 253 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 254 255 private static int NORMAL_FONT_SIZE = 12; 256 private static int EVENT_TEXT_FONT_SIZE = 12; 257 private static int HOURS_FONT_SIZE = 12; 258 private static int AMPM_FONT_SIZE = 9; 259 private static int MIN_CELL_WIDTH_FOR_TEXT = 27; 260 private static final int MAX_EVENT_TEXT_LEN = 500; 261 private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels 262 263 // This value forces the position calculator to take care of the overwap which can't be 264 // detected from the view of event time but actually is detected when rendering them. 265 // 266 // Detail: 267 // Imagine there are two events: A (from 1:00pm to 1:01pm) and B (from 1:02pm to 2:00pm). 268 // The position calculator (Event#doComputePositions()), marks them as "not overwrapped" 269 // as A finishes before B's begin time, so those events are put on the same column 270 // (or, horizontal position). 271 // From the view of renderer, however, the actual rectangle for A is larger than "1 min." 272 // for accomodating at least 1 line of text in it. 273 // As a result, A's rectangle is overwrapped by B's, and A becomes hard to be touched 274 // without trackball or DPAD (as, it is beneath B from the user' view). 275 // This values forces the original calculator to take care of the actual overwrap detected in 276 // rendering time. 277 // 278 // Note: 279 // Theoretically we can calcurate an ideal value for this purpose by making the calculator 280 // understand the relation between each event and pixel-level height of actual rectangles, 281 // but we don't do so as currently the calculator doesn't have convenient way to obtain 282 // necessary values for the calculation. 283 /* package */ static long EVENT_OVERWRAP_MARGIN_TIME = MILLIS_PER_MINUTE * 15; 284 285 private static int mSelectionColor; 286 private static int mPressedColor; 287 private static int mSelectedEventTextColor; 288 private static int mEventTextColor; 289 private static int mWeek_saturdayColor; 290 private static int mWeek_sundayColor; 291 private static int mCalendarDateBannerTextColor; 292 private static int mCalendarAllDayBackground; 293 private static int mCalendarAmPmLabel; 294 private static int mCalendarDateBannerBackground; 295 private static int mCalendarDateSelected; 296 private static int mCalendarGridAreaBackground; 297 private static int mCalendarGridAreaSelected; 298 private static int mCalendarGridLineHorizontalColor; 299 private static int mCalendarGridLineVerticalColor; 300 private static int mCalendarHourBackground; 301 private static int mCalendarHourLabel; 302 private static int mCalendarHourSelected; 303 private static int mCurrentTimeMarkerColor; 304 private static int mCurrentTimeLineColor; 305 private static int mCurrentTimeMarkerBorderColor; 306 307 private int mViewStartX; 308 private int mViewStartY; 309 private int mMaxViewStartY; 310 private int mBitmapHeight; 311 private int mViewHeight; 312 private int mViewWidth; 313 private int mGridAreaHeight; 314 private int mCellHeight; 315 private int mScrollStartY; 316 private int mPreviousDirection; 317 private int mPreviousDistanceX; 318 319 private int mHoursTextHeight; 320 private int mEventTextAscent; 321 private int mEventTextHeight; 322 private int mAllDayHeight; 323 private int mBannerPlusMargin; 324 private int mMaxAllDayEvents; 325 326 protected int mNumDays = 7; 327 private int mNumHours = 10; 328 private int mHoursWidth; 329 private int mDateStrWidth; 330 private int mFirstCell; 331 private int mFirstHour = -1; 332 private int mFirstHourOffset; 333 private String[] mHourStrs; 334 private String[] mDayStrs; 335 private String[] mDayStrs2Letter; 336 private boolean mIs24HourFormat; 337 338 private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN]; 339 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); 340 private boolean mComputeSelectedEvents; 341 private Event mSelectedEvent; 342 private Event mPrevSelectedEvent; 343 private Rect mPrevBox = new Rect(); 344 protected final Resources mResources; 345 private String mAmString; 346 private String mPmString; 347 private DeleteEventHelper mDeleteEventHelper; 348 349 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); 350 351 /** 352 * The initial state of the touch mode when we enter this view. 353 */ 354 private static final int TOUCH_MODE_INITIAL_STATE = 0; 355 356 /** 357 * Indicates we just received the touch event and we are waiting to see if 358 * it is a tap or a scroll gesture. 359 */ 360 private static final int TOUCH_MODE_DOWN = 1; 361 362 /** 363 * Indicates the touch gesture is a vertical scroll 364 */ 365 private static final int TOUCH_MODE_VSCROLL = 0x20; 366 367 /** 368 * Indicates the touch gesture is a horizontal scroll 369 */ 370 private static final int TOUCH_MODE_HSCROLL = 0x40; 371 372 private int mTouchMode = TOUCH_MODE_INITIAL_STATE; 373 374 /** 375 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. 376 */ 377 private static final int SELECTION_HIDDEN = 0; 378 private static final int SELECTION_PRESSED = 1; 379 private static final int SELECTION_SELECTED = 2; 380 private static final int SELECTION_LONGPRESS = 3; 381 382 private int mSelectionMode = SELECTION_HIDDEN; 383 384 private boolean mScrolling = false; 385 386 private TimeZone mTimeZone; 387 private String mDateRange; 388 private TextView mTitleTextView; 389 390 // Accessibility support related members 391 392 private int mPrevSelectionDay; 393 private int mPrevSelectionHour; 394 private CharSequence mPrevTitleTextViewText; 395 private Bundle mTempEventBundle; 396 397 public CalendarView(CalendarActivity activity) { 398 super(activity); 399 if (mScale == 0) { 400 mScale = getContext().getResources().getDisplayMetrics().density; 401 if (mScale != 1) { 402 SINGLE_ALLDAY_HEIGHT *= mScale; 403 MAX_ALLDAY_HEIGHT *= mScale; 404 ALLDAY_TOP_MARGIN *= mScale; 405 MAX_ALLDAY_EVENT_HEIGHT *= mScale; 406 407 NORMAL_FONT_SIZE *= mScale; 408 EVENT_TEXT_FONT_SIZE *= mScale; 409 HOURS_FONT_SIZE *= mScale; 410 AMPM_FONT_SIZE *= mScale; 411 MIN_CELL_WIDTH_FOR_TEXT *= mScale; 412 MIN_EVENT_HEIGHT *= mScale; 413 414 HORIZONTAL_SCROLL_THRESHOLD *= mScale; 415 416 CURRENT_TIME_MARKER_HEIGHT *= mScale; 417 CURRENT_TIME_MARKER_WIDTH *= mScale; 418 CURRENT_TIME_LINE_HEIGHT *= mScale; 419 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale; 420 CURRENT_TIME_MARKER_INNER_WIDTH *= mScale; 421 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale; 422 423 SMALL_ROUND_RADIUS *= mScale; 424 } 425 } 426 427 mResources = activity.getResources(); 428 mEventLoader = activity.mEventLoader; 429 mEventGeometry = new EventGeometry(); 430 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); 431 mEventGeometry.setHourGap(HOUR_GAP); 432 mParentActivity = activity; 433 mCalendarApp = (CalendarApplication) mParentActivity.getApplication(); 434 mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */); 435 mLastPopupEventID = INVALID_EVENT_ID; 436 437 init(activity); 438 } 439 440 private void init(Context context) { 441 setFocusable(true); 442 443 // Allow focus in touch mode so that we can do keyboard shortcuts 444 // even after we've entered touch mode. 445 setFocusableInTouchMode(true); 446 setClickable(true); 447 setOnCreateContextMenuListener(this); 448 449 mStartDay = Utils.getFirstDayOfWeek(); 450 451 mTimeZone = TimeZone.getTimeZone(Utils.getTimeZone(context, mUpdateTZ)); 452 453 mContext = context; 454 mCurrentTime = new Time(Utils.getTimeZone(context, mUpdateTZ)); 455 long currentTime = System.currentTimeMillis(); 456 mCurrentTime.set(currentTime); 457 //The % makes it go off at the next increment of 5 minutes. 458 postDelayed(mUpdateCurrentTime, 459 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 460 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 461 462 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); 463 mWeek_sundayColor = mResources.getColor(R.color.week_sunday); 464 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); 465 mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background); 466 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); 467 mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background); 468 mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected); 469 mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background); 470 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); 471 mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color); 472 mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color); 473 mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background); 474 mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label); 475 mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected); 476 mSelectionColor = mResources.getColor(R.color.selection); 477 mPressedColor = mResources.getColor(R.color.pressed); 478 mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color); 479 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); 480 mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker); 481 mCurrentTimeLineColor = mResources.getColor(R.color.current_time_line); 482 mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border); 483 mEventTextPaint.setColor(mEventTextColor); 484 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); 485 mEventTextPaint.setTextAlign(Paint.Align.LEFT); 486 mEventTextPaint.setAntiAlias(true); 487 488 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); 489 Paint p = mSelectionPaint; 490 p.setColor(gridLineColor); 491 p.setStyle(Style.STROKE); 492 p.setStrokeWidth(2.0f); 493 p.setAntiAlias(false); 494 495 p = mPaint; 496 p.setAntiAlias(true); 497 498 mPaintBorder.setColor(0xffc8c8c8); 499 mPaintBorder.setStyle(Style.STROKE); 500 mPaintBorder.setAntiAlias(true); 501 mPaintBorder.setStrokeWidth(2.0f); 502 503 // Allocate space for 2 weeks worth of weekday names so that we can 504 // easily start the week display at any week day. 505 mDayStrs = new String[14]; 506 507 // Also create an array of 2-letter abbreviations. 508 mDayStrs2Letter = new String[14]; 509 510 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 511 int index = i - Calendar.SUNDAY; 512 // e.g. Tue for Tuesday 513 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM); 514 mDayStrs[index + 7] = mDayStrs[index]; 515 // e.g. Tu for Tuesday 516 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT); 517 518 // If we don't have 2-letter day strings, fall back to 1-letter. 519 if (mDayStrs2Letter[index].equals(mDayStrs[index])) { 520 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); 521 } 522 523 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; 524 } 525 526 // Figure out how much space we need for the 3-letter abbrev names 527 // in the worst case. 528 p.setTextSize(NORMAL_FONT_SIZE); 529 p.setTypeface(mBold); 530 String[] dateStrs = {" 28", " 30"}; 531 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); 532 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); 533 534 p.setTextSize(HOURS_FONT_SIZE); 535 p.setTypeface(null); 536 updateIs24HourFormat(); 537 538 mAmString = DateUtils.getAMPMString(Calendar.AM); 539 mPmString = DateUtils.getAMPMString(Calendar.PM); 540 String[] ampm = {mAmString, mPmString}; 541 p.setTextSize(AMPM_FONT_SIZE); 542 mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p); 543 mHoursWidth += HOURS_MARGIN; 544 545 LayoutInflater inflater; 546 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 547 mPopupView = inflater.inflate(R.layout.bubble_event, null); 548 mPopupView.setLayoutParams(new ViewGroup.LayoutParams( 549 ViewGroup.LayoutParams.MATCH_PARENT, 550 ViewGroup.LayoutParams.WRAP_CONTENT)); 551 mPopup = new PopupWindow(context); 552 mPopup.setContentView(mPopupView); 553 Resources.Theme dialogTheme = getResources().newTheme(); 554 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); 555 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { 556 android.R.attr.windowBackground }); 557 mPopup.setBackgroundDrawable(ta.getDrawable(0)); 558 ta.recycle(); 559 560 // Enable touching the popup window 561 mPopupView.setOnClickListener(this); 562 563 mBaseDate = new Time(Utils.getTimeZone(context, mUpdateTZ)); 564 long millis = System.currentTimeMillis(); 565 mBaseDate.set(millis); 566 567 mEarliestStartHour = new int[mNumDays]; 568 mHasAllDayEvent = new boolean[mNumDays]; 569 570 mNumHours = context.getResources().getInteger(R.integer.number_of_hours); 571 mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title); 572 } 573 574 /** 575 * This is called when the popup window is pressed. 576 */ 577 public void onClick(View v) { 578 if (v == mPopupView) { 579 // Pretend it was a trackball click because that will always 580 // jump to the "View event" screen. 581 switchViews(true /* trackball */); 582 } 583 } 584 585 public void updateIs24HourFormat() { 586 mIs24HourFormat = DateFormat.is24HourFormat(mParentActivity); 587 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; 588 } 589 590 /** 591 * Returns the start of the selected time in milliseconds since the epoch. 592 * 593 * @return selected time in UTC milliseconds since the epoch. 594 */ 595 long getSelectedTimeInMillis() { 596 Time time = new Time(mBaseDate); 597 time.setJulianDay(mSelectionDay); 598 time.hour = mSelectionHour; 599 600 // We ignore the "isDst" field because we want normalize() to figure 601 // out the correct DST value and not adjust the selected time based 602 // on the current setting of DST. 603 return time.normalize(true /* ignore isDst */); 604 } 605 606 Time getSelectedTime() { 607 Time time = new Time(mBaseDate); 608 time.setJulianDay(mSelectionDay); 609 time.hour = mSelectionHour; 610 611 // We ignore the "isDst" field because we want normalize() to figure 612 // out the correct DST value and not adjust the selected time based 613 // on the current setting of DST. 614 time.normalize(true /* ignore isDst */); 615 return time; 616 } 617 618 /** 619 * Returns the start of the selected time in minutes since midnight, 620 * local time. The derived class must ensure that this is consistent 621 * with the return value from getSelectedTimeInMillis(). 622 */ 623 int getSelectedMinutesSinceMidnight() { 624 return mSelectionHour * MINUTES_PER_HOUR; 625 } 626 627 public void setSelectedDay(Time time) { 628 mBaseDate.set(time); 629 mSelectionHour = mBaseDate.hour; 630 mSelectedEvent = null; 631 mPrevSelectedEvent = null; 632 long millis = mBaseDate.toMillis(false /* use isDst */); 633 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff); 634 mSelectedEvents.clear(); 635 mComputeSelectedEvents = true; 636 637 // Force a recalculation of the first visible hour 638 mFirstHour = -1; 639 recalc(); 640 mTitleTextView.setText(mDateRange); 641 642 // Force a redraw of the selection box. 643 mSelectionMode = SELECTION_SELECTED; 644 mRedrawScreen = true; 645 mRemeasure = true; 646 invalidate(); 647 } 648 649 public Time getSelectedDay() { 650 Time time = new Time(mBaseDate); 651 time.setJulianDay(mSelectionDay); 652 time.hour = mSelectionHour; 653 654 // We ignore the "isDst" field because we want normalize() to figure 655 // out the correct DST value and not adjust the selected time based 656 // on the current setting of DST. 657 time.normalize(true /* ignore isDst */); 658 return time; 659 } 660 661 private void recalc() { 662 // Set the base date to the beginning of the week if we are displaying 663 // 7 days at a time. 664 if (mNumDays == 7) { 665 int dayOfWeek = mBaseDate.weekDay; 666 int diff = dayOfWeek - mStartDay; 667 if (diff != 0) { 668 if (diff < 0) { 669 diff += 7; 670 } 671 mBaseDate.monthDay -= diff; 672 mBaseDate.normalize(true /* ignore isDst */); 673 } 674 } 675 676 long start = mBaseDate.normalize(true /* use isDst */); 677 long end = start; 678 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); 679 mLastJulianDay = mFirstJulianDay + mNumDays - 1; 680 681 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); 682 mFirstDate = mBaseDate.monthDay; 683 684 int flags = DateUtils.FORMAT_SHOW_YEAR; 685 if (DateFormat.is24HourFormat(mParentActivity)) { 686 flags |= DateUtils.FORMAT_24HOUR; 687 } 688 if (mNumDays > 1) { 689 mBaseDate.monthDay += mNumDays - 1; 690 end = mBaseDate.toMillis(true /* ignore isDst */); 691 mBaseDate.monthDay -= mNumDays - 1; 692 flags |= DateUtils.FORMAT_NO_MONTH_DAY; 693 } else { 694 flags |= DateUtils.FORMAT_SHOW_WEEKDAY 695 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; 696 } 697 698 mDateRange = Utils.formatDateRange(mParentActivity, start, end, flags); 699 700 if (!TextUtils.equals(Utils.getTimeZone(mContext, mUpdateTZ), Time.getCurrentTimezone())) { 701 flags = DateUtils.FORMAT_SHOW_TIME; 702 if (DateFormat.is24HourFormat(mParentActivity)) { 703 flags |= DateUtils.FORMAT_24HOUR; 704 } 705 start = System.currentTimeMillis(); 706 707 String tz = Utils.getTimeZone(mContext, mUpdateTZ); 708 boolean isDST = mBaseDate.isDst != 0; 709 StringBuilder title = new StringBuilder(mDateRange); 710 title.append(" (").append(Utils.formatDateRange(mContext, start, start, flags)) 711 .append(" ") 712 .append(mTimeZone.getDisplayName(isDST, TimeZone.SHORT, Locale.getDefault())) 713 .append(")"); 714 mDateRange = title.toString(); 715 } 716 // Do not set the title here because this is called when executing 717 // initNextView() to prepare the Day view when sliding the finger 718 // horizontally but we don't always want to change the title. And 719 // if we change the title here and then change it back in the caller 720 // then we get an annoying flicker. 721 } 722 723 void setDetailedView(String detailedView) { 724 mDetailedView = detailedView; 725 } 726 727 @Override 728 protected void onSizeChanged(int width, int height, int oldw, int oldh) { 729 mViewWidth = width; 730 mViewHeight = height; 731 int gridAreaWidth = width - mHoursWidth; 732 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; 733 734 Paint p = new Paint(); 735 p.setTextSize(NORMAL_FONT_SIZE); 736 int bannerTextHeight = (int) Math.abs(p.ascent()); 737 738 p.setTextSize(HOURS_FONT_SIZE); 739 mHoursTextHeight = (int) Math.abs(p.ascent()); 740 741 p.setTextSize(EVENT_TEXT_FONT_SIZE); 742 float ascent = -p.ascent(); 743 mEventTextAscent = (int) Math.ceil(ascent); 744 float totalHeight = ascent + p.descent(); 745 mEventTextHeight = (int) Math.ceil(totalHeight); 746 747 if (mNumDays > 1) { 748 mBannerPlusMargin = bannerTextHeight + 14; 749 } else { 750 mBannerPlusMargin = 0; 751 } 752 753 remeasure(width, height); 754 } 755 756 // Measures the space needed for various parts of the view after 757 // loading new events. This can change if there are all-day events. 758 private void remeasure(int width, int height) { 759 760 // First, clear the array of earliest start times, and the array 761 // indicating presence of an all-day event. 762 for (int day = 0; day < mNumDays; day++) { 763 mEarliestStartHour[day] = 25; // some big number 764 mHasAllDayEvent[day] = false; 765 } 766 767 // Compute the space needed for the all-day events, if any. 768 // Make a pass over all the events, and keep track of the maximum 769 // number of all-day events in any one day. Also, keep track of 770 // the earliest event in each day. 771 int maxAllDayEvents = 0; 772 ArrayList<Event> events = mEvents; 773 int len = events.size(); 774 for (int ii = 0; ii < len; ii++) { 775 Event event = events.get(ii); 776 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) 777 continue; 778 if (event.allDay) { 779 int max = event.getColumn() + 1; 780 if (maxAllDayEvents < max) { 781 maxAllDayEvents = max; 782 } 783 int daynum = event.startDay - mFirstJulianDay; 784 int durationDays = event.endDay - event.startDay + 1; 785 if (daynum < 0) { 786 durationDays += daynum; 787 daynum = 0; 788 } 789 if (daynum + durationDays > mNumDays) { 790 durationDays = mNumDays - daynum; 791 } 792 for (int day = daynum; durationDays > 0; day++, durationDays--) { 793 mHasAllDayEvent[day] = true; 794 } 795 } else { 796 int daynum = event.startDay - mFirstJulianDay; 797 int hour = event.startTime / 60; 798 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 799 mEarliestStartHour[daynum] = hour; 800 } 801 802 // Also check the end hour in case the event spans more than 803 // one day. 804 daynum = event.endDay - mFirstJulianDay; 805 hour = event.endTime / 60; 806 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 807 mEarliestStartHour[daynum] = hour; 808 } 809 } 810 } 811 mMaxAllDayEvents = maxAllDayEvents; 812 813 mFirstCell = mBannerPlusMargin; 814 int allDayHeight = 0; 815 if (maxAllDayEvents > 0) { 816 // If there is at most one all-day event per day, then use less 817 // space (but more than the space for a single event). 818 if (maxAllDayEvents == 1) { 819 allDayHeight = SINGLE_ALLDAY_HEIGHT; 820 } else { 821 // Allow the all-day area to grow in height depending on the 822 // number of all-day events we need to show, up to a limit. 823 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT; 824 if (allDayHeight > MAX_ALLDAY_HEIGHT) { 825 allDayHeight = MAX_ALLDAY_HEIGHT; 826 } 827 } 828 mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN; 829 } else { 830 mSelectionAllDay = false; 831 } 832 mAllDayHeight = allDayHeight; 833 834 mGridAreaHeight = height - mFirstCell; 835 mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours; 836 int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP; 837 int bottomSpace = mGridAreaHeight - usedGridAreaHeight; 838 mEventGeometry.setHourHeight(mCellHeight); 839 840 // Create an off-screen bitmap that we can draw into. 841 mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace; 842 if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 && 843 mBitmapHeight > 0) { 844 if (mBitmap != null) { 845 mBitmap.recycle(); 846 } 847 mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565); 848 mCanvas = new Canvas(mBitmap); 849 } 850 mMaxViewStartY = mBitmapHeight - mGridAreaHeight; 851 852 if (mFirstHour == -1) { 853 initFirstHour(); 854 mFirstHourOffset = 0; 855 } 856 857 // When we change the base date, the number of all-day events may 858 // change and that changes the cell height. When we switch dates, 859 // we use the mFirstHourOffset from the previous view, but that may 860 // be too large for the new view if the cell height is smaller. 861 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { 862 mFirstHourOffset = mCellHeight + HOUR_GAP - 1; 863 } 864 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; 865 866 int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); 867 //When we get new events we don't want to dismiss the popup unless the event changes 868 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { 869 mPopup.dismiss(); 870 } 871 mPopup.setWidth(eventAreaWidth - 20); 872 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 873 } 874 875 /** 876 * Initialize the state for another view. The given view is one that has 877 * its own bitmap and will use an animation to replace the current view. 878 * The current view and new view are either both Week views or both Day 879 * views. They differ in their base date. 880 * 881 * @param view the view to initialize. 882 */ 883 private void initView(CalendarView view) { 884 view.mSelectionHour = mSelectionHour; 885 view.mSelectedEvents.clear(); 886 view.mComputeSelectedEvents = true; 887 view.mFirstHour = mFirstHour; 888 view.mFirstHourOffset = mFirstHourOffset; 889 view.remeasure(getWidth(), getHeight()); 890 891 view.mSelectedEvent = null; 892 view.mPrevSelectedEvent = null; 893 view.mStartDay = mStartDay; 894 if (view.mEvents.size() > 0) { 895 view.mSelectionAllDay = mSelectionAllDay; 896 } else { 897 view.mSelectionAllDay = false; 898 } 899 900 // Redraw the screen so that the selection box will be redrawn. We may 901 // have scrolled to a different part of the day in some other view 902 // so the selection box in this view may no longer be visible. 903 view.mRedrawScreen = true; 904 view.recalc(); 905 } 906 907 /** 908 * Switch to another view based on what was selected (an event or a free 909 * slot) and how it was selected (by touch or by trackball). 910 * 911 * @param trackBallSelection true if the selection was made using the 912 * trackball. 913 */ 914 private void switchViews(boolean trackBallSelection) { 915 Event selectedEvent = mSelectedEvent; 916 917 mPopup.dismiss(); 918 mLastPopupEventID = INVALID_EVENT_ID; 919 if (mNumDays > 1) { 920 // This is the Week view. 921 // With touch, we always switch to Day/Agenda View 922 // With track ball, if we selected a free slot, then create an event. 923 // If we selected a specific event, switch to EventInfo view. 924 if (trackBallSelection) { 925 if (selectedEvent == null) { 926 // Switch to the EditEvent view 927 long startMillis = getSelectedTimeInMillis(); 928 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 929 Intent intent = new Intent(Intent.ACTION_VIEW); 930 intent.setClassName(mParentActivity, EditEvent.class.getName()); 931 intent.putExtra(EVENT_BEGIN_TIME, startMillis); 932 intent.putExtra(EVENT_END_TIME, endMillis); 933 mParentActivity.startActivity(intent); 934 } else { 935 // Switch to the EventInfo view 936 Intent intent = new Intent(Intent.ACTION_VIEW); 937 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, 938 selectedEvent.id); 939 intent.setData(eventUri); 940 intent.setClassName(mParentActivity, EventInfoActivity.class.getName()); 941 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis); 942 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis); 943 mParentActivity.startActivity(intent); 944 } 945 } else { 946 // This was a touch selection. If the touch selected a single 947 // unambiguous event, then view that event. Otherwise go to 948 // Day/Agenda view. 949 if (mSelectedEvents.size() == 1) { 950 // Switch to the EventInfo view 951 Intent intent = new Intent(Intent.ACTION_VIEW); 952 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, 953 selectedEvent.id); 954 intent.setData(eventUri); 955 intent.setClassName(mParentActivity, EventInfoActivity.class.getName()); 956 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis); 957 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis); 958 mParentActivity.startActivity(intent); 959 } else { 960 // Switch to the Day/Agenda view. 961 long millis = getSelectedTimeInMillis(); 962 Utils.startActivity(mParentActivity, mDetailedView, millis); 963 } 964 } 965 } else { 966 // This is the Day view. 967 // If we selected a free slot, then create an event. 968 // If we selected an event, then go to the EventInfo view. 969 if (selectedEvent == null) { 970 // Switch to the EditEvent view 971 long startMillis = getSelectedTimeInMillis(); 972 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 973 Intent intent = new Intent(Intent.ACTION_VIEW); 974 intent.setClassName(mParentActivity, EditEvent.class.getName()); 975 intent.putExtra(EVENT_BEGIN_TIME, startMillis); 976 intent.putExtra(EVENT_END_TIME, endMillis); 977 mParentActivity.startActivity(intent); 978 } else { 979 // Switch to the EventInfo view 980 Intent intent = new Intent(Intent.ACTION_VIEW); 981 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id); 982 intent.setData(eventUri); 983 intent.setClassName(mParentActivity, EventInfoActivity.class.getName()); 984 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis); 985 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis); 986 mParentActivity.startActivity(intent); 987 } 988 } 989 } 990 991 @Override 992 public boolean onKeyUp(int keyCode, KeyEvent event) { 993 mScrolling = false; 994 long duration = event.getEventTime() - event.getDownTime(); 995 996 switch (keyCode) { 997 case KeyEvent.KEYCODE_DPAD_CENTER: 998 if (mSelectionMode == SELECTION_HIDDEN) { 999 // Don't do anything unless the selection is visible. 1000 break; 1001 } 1002 1003 if (mSelectionMode == SELECTION_PRESSED) { 1004 // This was the first press when there was nothing selected. 1005 // Change the selection from the "pressed" state to the 1006 // the "selected" state. We treat short-press and 1007 // long-press the same here because nothing was selected. 1008 mSelectionMode = SELECTION_SELECTED; 1009 mRedrawScreen = true; 1010 invalidate(); 1011 break; 1012 } 1013 1014 // Check the duration to determine if this was a short press 1015 if (duration < ViewConfiguration.getLongPressTimeout()) { 1016 switchViews(true /* trackball */); 1017 } else { 1018 mSelectionMode = SELECTION_LONGPRESS; 1019 mRedrawScreen = true; 1020 invalidate(); 1021 performLongClick(); 1022 } 1023 break; 1024 case KeyEvent.KEYCODE_BACK: 1025 if (event.isTracking() && !event.isCanceled()) { 1026 mPopup.dismiss(); 1027 mParentActivity.finish(); 1028 return true; 1029 } 1030 break; 1031 } 1032 return super.onKeyUp(keyCode, event); 1033 } 1034 1035 @Override 1036 public boolean onKeyDown(int keyCode, KeyEvent event) { 1037 if (mSelectionMode == SELECTION_HIDDEN) { 1038 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1039 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP 1040 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1041 // Display the selection box but don't move or select it 1042 // on this key press. 1043 mSelectionMode = SELECTION_SELECTED; 1044 mRedrawScreen = true; 1045 invalidate(); 1046 return true; 1047 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 1048 // Display the selection box but don't select it 1049 // on this key press. 1050 mSelectionMode = SELECTION_PRESSED; 1051 mRedrawScreen = true; 1052 invalidate(); 1053 return true; 1054 } 1055 } 1056 1057 mSelectionMode = SELECTION_SELECTED; 1058 mScrolling = false; 1059 boolean redraw; 1060 int selectionDay = mSelectionDay; 1061 1062 switch (keyCode) { 1063 case KeyEvent.KEYCODE_DEL: 1064 // Delete the selected event, if any 1065 Event selectedEvent = mSelectedEvent; 1066 if (selectedEvent == null) { 1067 return false; 1068 } 1069 mPopup.dismiss(); 1070 mLastPopupEventID = INVALID_EVENT_ID; 1071 1072 long begin = selectedEvent.startMillis; 1073 long end = selectedEvent.endMillis; 1074 long id = selectedEvent.id; 1075 mDeleteEventHelper.delete(begin, end, id, -1); 1076 return true; 1077 case KeyEvent.KEYCODE_ENTER: 1078 switchViews(true /* trackball or keyboard */); 1079 return true; 1080 case KeyEvent.KEYCODE_BACK: 1081 if (event.getRepeatCount() == 0) { 1082 event.startTracking(); 1083 return true; 1084 } 1085 return super.onKeyDown(keyCode, event); 1086 case KeyEvent.KEYCODE_DPAD_LEFT: 1087 if (mSelectedEvent != null) { 1088 mSelectedEvent = mSelectedEvent.nextLeft; 1089 } 1090 if (mSelectedEvent == null) { 1091 mLastPopupEventID = INVALID_EVENT_ID; 1092 selectionDay -= 1; 1093 } 1094 redraw = true; 1095 break; 1096 1097 case KeyEvent.KEYCODE_DPAD_RIGHT: 1098 if (mSelectedEvent != null) { 1099 mSelectedEvent = mSelectedEvent.nextRight; 1100 } 1101 if (mSelectedEvent == null) { 1102 mLastPopupEventID = INVALID_EVENT_ID; 1103 selectionDay += 1; 1104 } 1105 redraw = true; 1106 break; 1107 1108 case KeyEvent.KEYCODE_DPAD_UP: 1109 if (mSelectedEvent != null) { 1110 mSelectedEvent = mSelectedEvent.nextUp; 1111 } 1112 if (mSelectedEvent == null) { 1113 mLastPopupEventID = INVALID_EVENT_ID; 1114 if (!mSelectionAllDay) { 1115 mSelectionHour -= 1; 1116 adjustHourSelection(); 1117 mSelectedEvents.clear(); 1118 mComputeSelectedEvents = true; 1119 } 1120 } 1121 redraw = true; 1122 break; 1123 1124 case KeyEvent.KEYCODE_DPAD_DOWN: 1125 if (mSelectedEvent != null) { 1126 mSelectedEvent = mSelectedEvent.nextDown; 1127 } 1128 if (mSelectedEvent == null) { 1129 mLastPopupEventID = INVALID_EVENT_ID; 1130 if (mSelectionAllDay) { 1131 mSelectionAllDay = false; 1132 } else { 1133 mSelectionHour++; 1134 adjustHourSelection(); 1135 mSelectedEvents.clear(); 1136 mComputeSelectedEvents = true; 1137 } 1138 } 1139 redraw = true; 1140 break; 1141 1142 default: 1143 return super.onKeyDown(keyCode, event); 1144 } 1145 1146 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { 1147 boolean forward; 1148 CalendarView view = mParentActivity.getNextView(); 1149 Time date = view.mBaseDate; 1150 date.set(mBaseDate); 1151 if (selectionDay < mFirstJulianDay) { 1152 date.monthDay -= mNumDays; 1153 forward = false; 1154 } else { 1155 date.monthDay += mNumDays; 1156 forward = true; 1157 } 1158 date.normalize(true /* ignore isDst */); 1159 view.mSelectionDay = selectionDay; 1160 1161 initView(view); 1162 mTitleTextView.setText(view.mDateRange); 1163 mParentActivity.switchViews(forward, 0, 0); 1164 return true; 1165 } 1166 mSelectionDay = selectionDay; 1167 mSelectedEvents.clear(); 1168 mComputeSelectedEvents = true; 1169 1170 if (redraw) { 1171 mRedrawScreen = true; 1172 invalidate(); 1173 return true; 1174 } 1175 1176 return super.onKeyDown(keyCode, event); 1177 } 1178 1179 // This is called after scrolling stops to move the selected hour 1180 // to the visible part of the screen. 1181 private void resetSelectedHour() { 1182 if (mSelectionHour < mFirstHour + 1) { 1183 mSelectionHour = mFirstHour + 1; 1184 mSelectedEvent = null; 1185 mSelectedEvents.clear(); 1186 mComputeSelectedEvents = true; 1187 } else if (mSelectionHour > mFirstHour + mNumHours - 3) { 1188 mSelectionHour = mFirstHour + mNumHours - 3; 1189 mSelectedEvent = null; 1190 mSelectedEvents.clear(); 1191 mComputeSelectedEvents = true; 1192 } 1193 } 1194 1195 private void initFirstHour() { 1196 mFirstHour = mSelectionHour - mNumHours / 2; 1197 if (mFirstHour < 0) { 1198 mFirstHour = 0; 1199 } else if (mFirstHour + mNumHours > 24) { 1200 mFirstHour = 24 - mNumHours; 1201 } 1202 } 1203 1204 /** 1205 * Recomputes the first full hour that is visible on screen after the 1206 * screen is scrolled. 1207 */ 1208 private void computeFirstHour() { 1209 // Compute the first full hour that is visible on screen 1210 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); 1211 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; 1212 } 1213 1214 private void adjustHourSelection() { 1215 if (mSelectionHour < 0) { 1216 mSelectionHour = 0; 1217 if (mMaxAllDayEvents > 0) { 1218 mPrevSelectedEvent = null; 1219 mSelectionAllDay = true; 1220 } 1221 } 1222 1223 if (mSelectionHour > 23) { 1224 mSelectionHour = 23; 1225 } 1226 1227 // If the selected hour is at least 2 time slots from the top and 1228 // bottom of the screen, then don't scroll the view. 1229 if (mSelectionHour < mFirstHour + 1) { 1230 // If there are all-days events for the selected day but there 1231 // are no more normal events earlier in the day, then jump to 1232 // the all-day event area. 1233 // Exception 1: allow the user to scroll to 8am with the trackball 1234 // before jumping to the all-day event area. 1235 // Exception 2: if 12am is on screen, then allow the user to select 1236 // 12am before going up to the all-day event area. 1237 int daynum = mSelectionDay - mFirstJulianDay; 1238 if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1239 && mFirstHour > 0 && mFirstHour < 8) { 1240 mPrevSelectedEvent = null; 1241 mSelectionAllDay = true; 1242 mSelectionHour = mFirstHour + 1; 1243 return; 1244 } 1245 1246 if (mFirstHour > 0) { 1247 mFirstHour -= 1; 1248 mViewStartY -= (mCellHeight + HOUR_GAP); 1249 if (mViewStartY < 0) { 1250 mViewStartY = 0; 1251 } 1252 return; 1253 } 1254 } 1255 1256 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1257 if (mFirstHour < 24 - mNumHours) { 1258 mFirstHour += 1; 1259 mViewStartY += (mCellHeight + HOUR_GAP); 1260 if (mViewStartY > mBitmapHeight - mGridAreaHeight) { 1261 mViewStartY = mBitmapHeight - mGridAreaHeight; 1262 } 1263 return; 1264 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1265 mViewStartY = mBitmapHeight - mGridAreaHeight; 1266 } 1267 } 1268 } 1269 1270 void clearCachedEvents() { 1271 mLastReloadMillis = 0; 1272 } 1273 1274 private Runnable mCancelCallback = new Runnable() { 1275 public void run() { 1276 clearCachedEvents(); 1277 } 1278 }; 1279 1280 void reloadEvents() { 1281 // Protect against this being called before this view has been 1282 // initialized. 1283 if (mParentActivity == null) { 1284 return; 1285 } 1286 1287 mSelectedEvent = null; 1288 mPrevSelectedEvent = null; 1289 mSelectedEvents.clear(); 1290 1291 // The start date is the beginning of the week at 12am 1292 Time weekStart = new Time(Utils.getTimeZone(mContext, mUpdateTZ)); 1293 weekStart.set(mBaseDate); 1294 weekStart.hour = 0; 1295 weekStart.minute = 0; 1296 weekStart.second = 0; 1297 long millis = weekStart.normalize(true /* ignore isDst */); 1298 1299 // Avoid reloading events unnecessarily. 1300 if (millis == mLastReloadMillis) { 1301 return; 1302 } 1303 mLastReloadMillis = millis; 1304 1305 // load events in the background 1306 mParentActivity.startProgressSpinner(); 1307 final ArrayList<Event> events = new ArrayList<Event>(); 1308 mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() { 1309 public void run() { 1310 mEvents = events; 1311 mRemeasure = true; 1312 mRedrawScreen = true; 1313 mComputeSelectedEvents = true; 1314 recalc(); 1315 mParentActivity.stopProgressSpinner(); 1316 invalidate(); 1317 } 1318 }, mCancelCallback); 1319 } 1320 1321 @Override 1322 protected void onDraw(Canvas canvas) { 1323 if (mRemeasure) { 1324 remeasure(getWidth(), getHeight()); 1325 mRemeasure = false; 1326 } 1327 1328 if (mRedrawScreen && mCanvas != null) { 1329 doDraw(mCanvas); 1330 mRedrawScreen = false; 1331 } 1332 1333 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 1334 canvas.save(); 1335 if (mViewStartX > 0) { 1336 canvas.translate(mViewWidth - mViewStartX, 0); 1337 } else { 1338 canvas.translate(-(mViewWidth + mViewStartX), 0); 1339 } 1340 CalendarView nextView = mParentActivity.getNextView(); 1341 1342 // Prevent infinite recursive calls to onDraw(). 1343 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 1344 1345 nextView.onDraw(canvas); 1346 canvas.restore(); 1347 canvas.save(); 1348 canvas.translate(-mViewStartX, 0); 1349 } 1350 1351 if (mBitmap != null) { 1352 drawCalendarView(canvas); 1353 } 1354 1355 // Draw the fixed areas (that don't scroll) directly to the canvas. 1356 drawAfterScroll(canvas); 1357 mComputeSelectedEvents = false; 1358 1359 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 1360 canvas.restore(); 1361 } 1362 1363 sendAccessibilityEvents(); 1364 } 1365 1366 private void drawCalendarView(Canvas canvas) { 1367 1368 // Copy the scrollable region from the big bitmap to the canvas. 1369 Rect src = mSrcRect; 1370 Rect dest = mDestRect; 1371 1372 src.top = mViewStartY; 1373 src.bottom = mViewStartY + mGridAreaHeight; 1374 src.left = 0; 1375 src.right = mViewWidth; 1376 1377 dest.top = mFirstCell; 1378 dest.bottom = mViewHeight; 1379 dest.left = 0; 1380 dest.right = mViewWidth; 1381 1382 canvas.save(); 1383 canvas.clipRect(dest); 1384 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 1385 canvas.drawBitmap(mBitmap, src, dest, null); 1386 canvas.restore(); 1387 } 1388 1389 private void drawAfterScroll(Canvas canvas) { 1390 Paint p = mPaint; 1391 Rect r = mRect; 1392 1393 if (mMaxAllDayEvents != 0) { 1394 drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p); 1395 drawUpperLeftCorner(r, canvas, p); 1396 } 1397 1398 if (mNumDays > 1) { 1399 drawDayHeaderLoop(r, canvas, p); 1400 } 1401 1402 // Draw the AM and PM indicators if we're in 12 hour mode 1403 if (!mIs24HourFormat) { 1404 drawAmPm(canvas, p); 1405 } 1406 1407 // Update the popup window showing the event details, but only if 1408 // we are not scrolling and we have focus. 1409 if (!mScrolling && isFocused()) { 1410 updateEventDetails(); 1411 } 1412 } 1413 1414 // This isn't really the upper-left corner. It's the square area just 1415 // below the upper-left corner, above the hours and to the left of the 1416 // all-day area. 1417 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 1418 p.setColor(mCalendarHourBackground); 1419 r.top = mBannerPlusMargin; 1420 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN; 1421 r.left = 0; 1422 r.right = mHoursWidth; 1423 canvas.drawRect(r, p); 1424 } 1425 1426 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 1427 // Draw the horizontal day background banner 1428 p.setColor(mCalendarDateBannerBackground); 1429 r.top = 0; 1430 r.bottom = mBannerPlusMargin; 1431 r.left = 0; 1432 r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 1433 canvas.drawRect(r, p); 1434 1435 // Fill the extra space on the right side with the default background 1436 r.left = r.right; 1437 r.right = mViewWidth; 1438 p.setColor(mCalendarGridAreaBackground); 1439 canvas.drawRect(r, p); 1440 1441 // Draw a highlight on the selected day (if any), but only if we are 1442 // displaying more than one day. 1443 if (mSelectionMode != SELECTION_HIDDEN) { 1444 if (mNumDays > 1) { 1445 p.setColor(mCalendarDateSelected); 1446 r.top = 0; 1447 r.bottom = mBannerPlusMargin; 1448 int daynum = mSelectionDay - mFirstJulianDay; 1449 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP); 1450 r.right = r.left + mCellWidth; 1451 canvas.drawRect(r, p); 1452 } 1453 } 1454 1455 p.setTextSize(NORMAL_FONT_SIZE); 1456 p.setTextAlign(Paint.Align.CENTER); 1457 int x = mHoursWidth; 1458 int deltaX = mCellWidth + DAY_GAP; 1459 int cell = mFirstJulianDay; 1460 1461 String[] dayNames; 1462 if (mDateStrWidth < mCellWidth) { 1463 dayNames = mDayStrs; 1464 } else { 1465 dayNames = mDayStrs2Letter; 1466 } 1467 1468 p.setTypeface(mBold); 1469 p.setAntiAlias(true); 1470 for (int day = 0; day < mNumDays; day++, cell++) { 1471 drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p); 1472 x += deltaX; 1473 } 1474 } 1475 1476 private void drawAmPm(Canvas canvas, Paint p) { 1477 p.setColor(mCalendarAmPmLabel); 1478 p.setTextSize(AMPM_FONT_SIZE); 1479 p.setTypeface(mBold); 1480 p.setAntiAlias(true); 1481 mPaint.setTextAlign(Paint.Align.RIGHT); 1482 String text = mAmString; 1483 if (mFirstHour >= 12) { 1484 text = mPmString; 1485 } 1486 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 1487 int right = mHoursWidth - HOURS_RIGHT_MARGIN; 1488 canvas.drawText(text, right, y, p); 1489 1490 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 1491 // Also draw the "PM" 1492 text = mPmString; 1493 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 1494 + 2 * mHoursTextHeight + HOUR_GAP; 1495 canvas.drawText(text, right, y, p); 1496 } 1497 } 1498 1499 private void drawCurrentTimeMarker(int top, Canvas canvas, Paint p) { 1500 Rect r = new Rect(); 1501 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2; 1502 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2; 1503 r.left = 0; 1504 r.right = mHoursWidth; 1505 1506 p.setColor(mCurrentTimeMarkerColor); 1507 canvas.drawRect(r, p); 1508 } 1509 1510 private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) { 1511 //Do a white outline so it'll show up on a red event 1512 p.setColor(mCurrentTimeMarkerBorderColor); 1513 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH; 1514 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2 + CURRENT_TIME_LINE_BORDER_WIDTH; 1515 r.left = left + CURRENT_TIME_LINE_SIDE_BUFFER; 1516 r.right = r.left + mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER; 1517 canvas.drawRect(r, p); 1518 //Then draw the red line 1519 p.setColor(mCurrentTimeLineColor); 1520 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2; 1521 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2; 1522 canvas.drawRect(r, p); 1523 } 1524 1525 private void doDraw(Canvas canvas) { 1526 Paint p = mPaint; 1527 Rect r = mRect; 1528 int lineY = mCurrentTime.hour*(mCellHeight + HOUR_GAP) 1529 + ((mCurrentTime.minute * mCellHeight) / 60) 1530 + 1; 1531 1532 drawGridBackground(r, canvas, p); 1533 drawHours(r, canvas, p); 1534 1535 // Draw each day 1536 int x = mHoursWidth; 1537 int deltaX = mCellWidth + DAY_GAP; 1538 int cell = mFirstJulianDay; 1539 for (int day = 0; day < mNumDays; day++, cell++) { 1540 drawEvents(cell, x, HOUR_GAP, canvas, p); 1541 //If this is today 1542 if(cell == mTodayJulianDay) { 1543 //And the current time shows up somewhere on the screen 1544 if(lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 1545 //draw both the marker and the line 1546 drawCurrentTimeMarker(lineY, canvas, p); 1547 drawCurrentTimeLine(r, x, lineY, canvas, p); 1548 } 1549 } 1550 x += deltaX; 1551 } 1552 } 1553 1554 private void drawHours(Rect r, Canvas canvas, Paint p) { 1555 // Draw the background for the hour labels 1556 p.setColor(mCalendarHourBackground); 1557 r.top = 0; 1558 r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP; 1559 r.left = 0; 1560 r.right = mHoursWidth; 1561 canvas.drawRect(r, p); 1562 1563 // Fill the bottom left corner with the default grid background 1564 r.top = r.bottom; 1565 r.bottom = mBitmapHeight; 1566 p.setColor(mCalendarGridAreaBackground); 1567 canvas.drawRect(r, p); 1568 1569 // Draw a highlight on the selected hour (if needed) 1570 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) { 1571 p.setColor(mCalendarHourSelected); 1572 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 1573 r.bottom = r.top + mCellHeight + 2 * HOUR_GAP; 1574 r.left = 0; 1575 r.right = mHoursWidth; 1576 canvas.drawRect(r, p); 1577 1578 boolean drawBorder = false; 1579 if (!drawBorder) { 1580 r.top += HOUR_GAP; 1581 r.bottom -= HOUR_GAP; 1582 } 1583 1584 // Also draw the highlight on the grid 1585 p.setColor(mCalendarGridAreaSelected); 1586 int daynum = mSelectionDay - mFirstJulianDay; 1587 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP); 1588 r.right = r.left + mCellWidth; 1589 canvas.drawRect(r, p); 1590 1591 // Draw a border around the highlighted grid hour. 1592 if (drawBorder) { 1593 Path path = mPath; 1594 r.top += HOUR_GAP; 1595 r.bottom -= HOUR_GAP; 1596 path.reset(); 1597 path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW); 1598 canvas.drawPath(path, mSelectionPaint); 1599 } 1600 1601 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 1602 } 1603 1604 p.setColor(mCalendarHourLabel); 1605 p.setTextSize(HOURS_FONT_SIZE); 1606 p.setTypeface(mBold); 1607 p.setTextAlign(Paint.Align.RIGHT); 1608 p.setAntiAlias(true); 1609 1610 int right = mHoursWidth - HOURS_RIGHT_MARGIN; 1611 int y = HOUR_GAP + mHoursTextHeight; 1612 1613 for (int i = 0; i < 24; i++) { 1614 String time = mHourStrs[i]; 1615 canvas.drawText(time, right, y, p); 1616 y += mCellHeight + HOUR_GAP; 1617 } 1618 } 1619 1620 private void sendAccessibilityEvents() { 1621 if (!isShown() || !AccessibilityManager.getInstance(mContext).isEnabled()) { 1622 return; 1623 } 1624 // if the title text has changed => announce period 1625 CharSequence titleTextViewText = mTitleTextView.getText(); 1626 // intended use of identity comparison 1627 boolean titleChanged = titleTextViewText != mPrevTitleTextViewText; 1628 if (titleChanged) { 1629 mPrevTitleTextViewText = titleTextViewText; 1630 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 1631 } 1632 // if title or selection has changed => announce selection 1633 // Note: if the title has changed we want to send both events 1634 if (titleChanged || mPrevSelectionDay != mSelectionDay 1635 || mPrevSelectionHour != mSelectionHour) { 1636 mPrevSelectionDay = mSelectionDay; 1637 mPrevSelectionHour = mSelectionHour; 1638 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1639 } 1640 } 1641 1642 @Override 1643 public void sendAccessibilityEvent(int eventType) { 1644 // we send only selection events since semantically we select 1645 // certain element and not always this view gets focus which 1646 // triggers firing of a focus accessibility event 1647 if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) { 1648 return; 1649 } 1650 super.sendAccessibilityEvent(eventType); 1651 } 1652 1653 @Override 1654 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1655 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 1656 // add the currently shown period (day/week) 1657 if (mNumDays == 1) { 1658 // for daily view the title has enough context information 1659 event.getText().add(mTitleTextView.getText()); 1660 } else { 1661 // since the title view does not contain enough context we 1662 // compute a more descriptive title for the shown time frame 1663 int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH 1664 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY 1665 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 1666 if (DateFormat.is24HourFormat(mParentActivity)) { 1667 flags |= DateUtils.FORMAT_24HOUR; 1668 } 1669 1670 long start = mBaseDate.toMillis(false); 1671 long gmtOff = mBaseDate.gmtoff; 1672 int firstJulianDay = Time.getJulianDay(start, gmtOff); 1673 1674 Time time = new Time(mBaseDate); 1675 time.setJulianDay(firstJulianDay); 1676 long startTime = time.normalize(true); 1677 time.setJulianDay(firstJulianDay + mNumDays); 1678 long endTime = time.normalize(true); 1679 1680 String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime, 1681 flags); 1682 event.getText().add(timeRange); 1683 } 1684 } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { 1685 int flags = 0; 1686 // add the selection 1687 if (mNumDays == 1) { 1688 // if day view we need only hour information 1689 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 1690 } else { 1691 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 1692 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 1693 } 1694 long startTime = getSelectedTimeInMillis(); 1695 long endTime = startTime + MILLIS_PER_HOUR; 1696 if (DateFormat.is24HourFormat(mParentActivity)) { 1697 flags |= DateUtils.FORMAT_24HOUR; 1698 } 1699 String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime, 1700 flags); 1701 event.getText().add(timeRange); 1702 1703 // add the selected event data if such 1704 if (mSelectedEvent != null) { 1705 Event selectedEvent = mSelectedEvent; 1706 if (mTempEventBundle == null) { 1707 mTempEventBundle = new Bundle(); 1708 } 1709 Bundle bundle = mTempEventBundle; 1710 bundle.clear(); 1711 bundle.putLong("id", selectedEvent.id); 1712 bundle.putInt("color", selectedEvent.color); 1713 bundle.putCharSequence("title", selectedEvent.title); 1714 bundle.putCharSequence("location", selectedEvent.location); 1715 bundle.putBoolean("allDay", selectedEvent.allDay); 1716 bundle.putInt("startDay", selectedEvent.startDay); 1717 bundle.putInt("endDay", selectedEvent.endDay); 1718 bundle.putInt("startTime", selectedEvent.startTime); 1719 bundle.putInt("endTime", selectedEvent.endTime); 1720 bundle.putLong("startMillis", selectedEvent.startMillis); 1721 bundle.putLong("endMillis", selectedEvent.endMillis); 1722 bundle.putString("organizer", selectedEvent.organizer); 1723 bundle.putBoolean("guestsCanModify", selectedEvent.guestsCanModify); 1724 event.setParcelableData(bundle); 1725 } 1726 } 1727 1728 // add day event count, events for same hour count and 1729 // the index of the selected event for the same hour 1730 int todayEventCount = 0; 1731 int sameHourEventCount = 0; 1732 int currentSameHourEventIndex = 0; 1733 int selectionHourStart = mSelectionHour * MINUTES_PER_HOUR; 1734 int selectionHourEnd = selectionHourStart + MINUTES_PER_HOUR; 1735 for (int i = 0, count = mEvents.size(); i < count; i++) { 1736 Event calendarEvent = mEvents.get(i); 1737 if (calendarEvent.endDay == mSelectionDay) { 1738 todayEventCount++; 1739 if (selectionHourStart >= calendarEvent.endTime 1740 || selectionHourEnd <= calendarEvent.startTime) { 1741 continue; 1742 } 1743 if (calendarEvent == mSelectedEvent) { 1744 currentSameHourEventIndex = sameHourEventCount; 1745 } 1746 sameHourEventCount++; 1747 } 1748 } 1749 event.setAddedCount(todayEventCount); 1750 event.setItemCount(sameHourEventCount); 1751 event.setCurrentItemIndex(currentSameHourEventIndex); 1752 1753 return true; 1754 } 1755 1756 private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) { 1757 float xCenter = x + mCellWidth / 2.0f; 1758 1759 if (Utils.isSaturday(day, mStartDay)) { 1760 p.setColor(mWeek_saturdayColor); 1761 } else if (Utils.isSunday(day, mStartDay)) { 1762 p.setColor(mWeek_sundayColor); 1763 } else { 1764 p.setColor(mCalendarDateBannerTextColor); 1765 } 1766 1767 int dateNum = mFirstDate + day; 1768 if (dateNum > mMonthLength) { 1769 dateNum -= mMonthLength; 1770 } 1771 1772 String dateNumStr; 1773 // Add a leading zero if the date is a single digit 1774 if (dateNum < 10) { 1775 dateNumStr = "0" + dateNum; 1776 } else { 1777 dateNumStr = String.valueOf(dateNum); 1778 } 1779 1780 DayHeader header = dayHeaders[day]; 1781 if (header == null || header.cell != cell) { 1782 // The day header string is regenerated on every draw during drag and fling animation. 1783 // Caching day header since formatting the string takes surprising long time. 1784 1785 dayHeaders[day] = new DayHeader(); 1786 dayHeaders[day].cell = cell; 1787 dayHeaders[day].dateString = getResources().getString( 1788 R.string.weekday_day, dateStr, dateNumStr); 1789 } 1790 dateStr = dayHeaders[day].dateString; 1791 1792 float y = mBannerPlusMargin - 7; 1793 canvas.drawText(dateStr, xCenter, y, p); 1794 } 1795 1796 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 1797 Paint.Style savedStyle = p.getStyle(); 1798 1799 // Clear the background 1800 p.setColor(mCalendarGridAreaBackground); 1801 r.top = 0; 1802 r.bottom = mBitmapHeight; 1803 r.left = 0; 1804 r.right = mViewWidth; 1805 canvas.drawRect(r, p); 1806 1807 // Draw the horizontal grid lines 1808 p.setColor(mCalendarGridLineHorizontalColor); 1809 p.setStyle(Style.STROKE); 1810 p.setStrokeWidth(0); 1811 p.setAntiAlias(false); 1812 float startX = mHoursWidth; 1813 float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays; 1814 float y = 0; 1815 float deltaY = mCellHeight + HOUR_GAP; 1816 for (int hour = 0; hour <= 24; hour++) { 1817 canvas.drawLine(startX, y, stopX, y, p); 1818 y += deltaY; 1819 } 1820 1821 // Draw the vertical grid lines 1822 p.setColor(mCalendarGridLineVerticalColor); 1823 float startY = 0; 1824 float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 1825 float deltaX = mCellWidth + DAY_GAP; 1826 float x = mHoursWidth + mCellWidth; 1827 for (int day = 0; day < mNumDays; day++) { 1828 canvas.drawLine(x, startY, x, stopY, p); 1829 x += deltaX; 1830 } 1831 1832 // Restore the saved style. 1833 p.setStyle(savedStyle); 1834 p.setAntiAlias(true); 1835 } 1836 1837 Event getSelectedEvent() { 1838 if (mSelectedEvent == null) { 1839 // There is no event at the selected hour, so create a new event. 1840 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 1841 getSelectedMinutesSinceMidnight()); 1842 } 1843 return mSelectedEvent; 1844 } 1845 1846 boolean isEventSelected() { 1847 return (mSelectedEvent != null); 1848 } 1849 1850 Event getNewEvent() { 1851 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 1852 getSelectedMinutesSinceMidnight()); 1853 } 1854 1855 static Event getNewEvent(int julianDay, long utcMillis, 1856 int minutesSinceMidnight) { 1857 Event event = Event.newInstance(); 1858 event.startDay = julianDay; 1859 event.endDay = julianDay; 1860 event.startMillis = utcMillis; 1861 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 1862 event.startTime = minutesSinceMidnight; 1863 event.endTime = event.startTime + MINUTES_PER_HOUR; 1864 return event; 1865 } 1866 1867 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 1868 float maxWidthF = 0.0f; 1869 1870 int len = strings.length; 1871 for (int i = 0; i < len; i++) { 1872 float width = p.measureText(strings[i]); 1873 maxWidthF = Math.max(width, maxWidthF); 1874 } 1875 int maxWidth = (int) (maxWidthF + 0.5); 1876 if (maxWidth < currentMax) { 1877 maxWidth = currentMax; 1878 } 1879 return maxWidth; 1880 } 1881 1882 private void saveSelectionPosition(float left, float top, float right, float bottom) { 1883 mPrevBox.left = (int) left; 1884 mPrevBox.right = (int) right; 1885 mPrevBox.top = (int) top; 1886 mPrevBox.bottom = (int) bottom; 1887 } 1888 1889 private Rect getCurrentSelectionPosition() { 1890 Rect box = new Rect(); 1891 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 1892 box.bottom = box.top + mCellHeight + HOUR_GAP; 1893 int daynum = mSelectionDay - mFirstJulianDay; 1894 box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP); 1895 box.right = box.left + mCellWidth + DAY_GAP; 1896 return box; 1897 } 1898 1899 private void drawAllDayEvents(int firstDay, int numDays, 1900 Rect r, Canvas canvas, Paint p) { 1901 p.setTextSize(NORMAL_FONT_SIZE); 1902 p.setTextAlign(Paint.Align.LEFT); 1903 Paint eventTextPaint = mEventTextPaint; 1904 1905 // Draw the background for the all-day events area 1906 r.top = mBannerPlusMargin; 1907 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN; 1908 r.left = mHoursWidth; 1909 r.right = r.left + mNumDays * (mCellWidth + DAY_GAP); 1910 p.setColor(mCalendarAllDayBackground); 1911 canvas.drawRect(r, p); 1912 1913 // Fill the extra space on the right side with the default background 1914 r.left = r.right; 1915 r.right = mViewWidth; 1916 p.setColor(mCalendarGridAreaBackground); 1917 canvas.drawRect(r, p); 1918 1919 // Draw the vertical grid lines 1920 p.setColor(mCalendarGridLineVerticalColor); 1921 p.setStyle(Style.STROKE); 1922 p.setStrokeWidth(0); 1923 p.setAntiAlias(false); 1924 float startY = r.top; 1925 float stopY = r.bottom; 1926 float deltaX = mCellWidth + DAY_GAP; 1927 float x = mHoursWidth + mCellWidth; 1928 for (int day = 0; day <= mNumDays; day++) { 1929 canvas.drawLine(x, startY, x, stopY, p); 1930 x += deltaX; 1931 } 1932 p.setAntiAlias(true); 1933 p.setStyle(Style.FILL); 1934 1935 int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN; 1936 float left = mHoursWidth; 1937 int lastDay = firstDay + numDays - 1; 1938 ArrayList<Event> events = mEvents; 1939 int numEvents = events.size(); 1940 float drawHeight = mAllDayHeight; 1941 float numRectangles = mMaxAllDayEvents; 1942 for (int i = 0; i < numEvents; i++) { 1943 Event event = events.get(i); 1944 if (!event.allDay) 1945 continue; 1946 int startDay = event.startDay; 1947 int endDay = event.endDay; 1948 if (startDay > lastDay || endDay < firstDay) 1949 continue; 1950 if (startDay < firstDay) 1951 startDay = firstDay; 1952 if (endDay > lastDay) 1953 endDay = lastDay; 1954 int startIndex = startDay - firstDay; 1955 int endIndex = endDay - firstDay; 1956 float height = drawHeight / numRectangles; 1957 1958 // Prevent a single event from getting too big 1959 if (height > MAX_ALLDAY_EVENT_HEIGHT) { 1960 height = MAX_ALLDAY_EVENT_HEIGHT; 1961 } 1962 1963 // Leave a one-pixel space between the vertical day lines and the 1964 // event rectangle. 1965 event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2; 1966 event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1; 1967 event.top = y + height * event.getColumn(); 1968 1969 // Multiply the height by 0.9 to leave a little gap between events 1970 event.bottom = event.top + height * 0.9f; 1971 1972 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint); 1973 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN); 1974 1975 // Check if this all-day event intersects the selected day 1976 if (mSelectionAllDay && mComputeSelectedEvents) { 1977 if (startDay <= mSelectionDay && endDay >= mSelectionDay) { 1978 mSelectedEvents.add(event); 1979 } 1980 } 1981 } 1982 1983 if (mSelectionAllDay) { 1984 // Compute the neighbors for the list of all-day events that 1985 // intersect the selected day. 1986 computeAllDayNeighbors(); 1987 if (mSelectedEvent != null) { 1988 Event event = mSelectedEvent; 1989 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint); 1990 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN); 1991 } 1992 1993 // Draw the highlight on the selected all-day area 1994 float top = mBannerPlusMargin + 1; 1995 float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1; 1996 int daynum = mSelectionDay - mFirstJulianDay; 1997 left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1; 1998 float right = left + mCellWidth + DAY_GAP - 1; 1999 if (mNumDays == 1) { 2000 // The Day view doesn't have a vertical line on the right. 2001 right -= 1; 2002 } 2003 Path path = mPath; 2004 path.reset(); 2005 path.addRect(left, top, right, bottom, Direction.CW); 2006 canvas.drawPath(path, mSelectionPaint); 2007 2008 // Set the selection position to zero so that when we move down 2009 // to the normal event area, we will highlight the topmost event. 2010 saveSelectionPosition(0f, 0f, 0f, 0f); 2011 } 2012 } 2013 2014 private void computeAllDayNeighbors() { 2015 int len = mSelectedEvents.size(); 2016 if (len == 0 || mSelectedEvent != null) { 2017 return; 2018 } 2019 2020 // First, clear all the links 2021 for (int ii = 0; ii < len; ii++) { 2022 Event ev = mSelectedEvents.get(ii); 2023 ev.nextUp = null; 2024 ev.nextDown = null; 2025 ev.nextLeft = null; 2026 ev.nextRight = null; 2027 } 2028 2029 // For each event in the selected event list "mSelectedEvents", find 2030 // its neighbors in the up and down directions. This could be done 2031 // more efficiently by sorting on the Event.getColumn() field, but 2032 // the list is expected to be very small. 2033 2034 // Find the event in the same row as the previously selected all-day 2035 // event, if any. 2036 int startPosition = -1; 2037 if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) { 2038 startPosition = mPrevSelectedEvent.getColumn(); 2039 } 2040 int maxPosition = -1; 2041 Event startEvent = null; 2042 Event maxPositionEvent = null; 2043 for (int ii = 0; ii < len; ii++) { 2044 Event ev = mSelectedEvents.get(ii); 2045 int position = ev.getColumn(); 2046 if (position == startPosition) { 2047 startEvent = ev; 2048 } else if (position > maxPosition) { 2049 maxPositionEvent = ev; 2050 maxPosition = position; 2051 } 2052 for (int jj = 0; jj < len; jj++) { 2053 if (jj == ii) { 2054 continue; 2055 } 2056 Event neighbor = mSelectedEvents.get(jj); 2057 int neighborPosition = neighbor.getColumn(); 2058 if (neighborPosition == position - 1) { 2059 ev.nextUp = neighbor; 2060 } else if (neighborPosition == position + 1) { 2061 ev.nextDown = neighbor; 2062 } 2063 } 2064 } 2065 if (startEvent != null) { 2066 mSelectedEvent = startEvent; 2067 } else { 2068 mSelectedEvent = maxPositionEvent; 2069 } 2070 } 2071 2072 RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) { 2073 // If this event is selected, then use the selection color 2074 if (mSelectedEvent == event) { 2075 // Also, remember the last selected event that we drew 2076 mPrevSelectedEvent = event; 2077 p.setColor(mSelectionColor); 2078 eventTextPaint.setColor(mSelectedEventTextColor); 2079 } else { 2080 // Use the normal color for all-day events 2081 p.setColor(event.color); 2082 eventTextPaint.setColor(mEventTextColor); 2083 } 2084 2085 RectF rf = mRectF; 2086 rf.top = event.top; 2087 rf.bottom = event.bottom; 2088 rf.left = event.left; 2089 rf.right = event.right; 2090 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p); 2091 2092 rf.left += 2; 2093 rf.right -= 2; 2094 return rf; 2095 } 2096 2097 private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) { 2098 Paint eventTextPaint = mEventTextPaint; 2099 int cellWidth = mCellWidth; 2100 int cellHeight = mCellHeight; 2101 2102 // Use the selected hour as the selection region 2103 Rect selectionArea = mRect; 2104 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); 2105 selectionArea.bottom = selectionArea.top + cellHeight; 2106 selectionArea.left = left; 2107 selectionArea.right = selectionArea.left + cellWidth; 2108 2109 ArrayList<Event> events = mEvents; 2110 int numEvents = events.size(); 2111 EventGeometry geometry = mEventGeometry; 2112 2113 for (int i = 0; i < numEvents; i++) { 2114 Event event = events.get(i); 2115 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 2116 continue; 2117 } 2118 2119 if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents 2120 && geometry.eventIntersectsSelection(event, selectionArea)) { 2121 mSelectedEvents.add(event); 2122 } 2123 2124 RectF rf = drawEventRect(event, canvas, p, eventTextPaint); 2125 drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN); 2126 } 2127 2128 if (date == mSelectionDay && !mSelectionAllDay && isFocused() 2129 && mSelectionMode != SELECTION_HIDDEN) { 2130 computeNeighbors(); 2131 if (mSelectedEvent != null) { 2132 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint); 2133 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN); 2134 } 2135 } 2136 } 2137 2138 // Computes the "nearest" neighbor event in four directions (left, right, 2139 // up, down) for each of the events in the mSelectedEvents array. 2140 private void computeNeighbors() { 2141 int len = mSelectedEvents.size(); 2142 if (len == 0 || mSelectedEvent != null) { 2143 return; 2144 } 2145 2146 // First, clear all the links 2147 for (int ii = 0; ii < len; ii++) { 2148 Event ev = mSelectedEvents.get(ii); 2149 ev.nextUp = null; 2150 ev.nextDown = null; 2151 ev.nextLeft = null; 2152 ev.nextRight = null; 2153 } 2154 2155 Event startEvent = mSelectedEvents.get(0); 2156 int startEventDistance1 = 100000; // any large number 2157 int startEventDistance2 = 100000; // any large number 2158 int prevLocation = FROM_NONE; 2159 int prevTop; 2160 int prevBottom; 2161 int prevLeft; 2162 int prevRight; 2163 int prevCenter = 0; 2164 Rect box = getCurrentSelectionPosition(); 2165 if (mPrevSelectedEvent != null) { 2166 prevTop = (int) mPrevSelectedEvent.top; 2167 prevBottom = (int) mPrevSelectedEvent.bottom; 2168 prevLeft = (int) mPrevSelectedEvent.left; 2169 prevRight = (int) mPrevSelectedEvent.right; 2170 // Check if the previously selected event intersects the previous 2171 // selection box. (The previously selected event may be from a 2172 // much older selection box.) 2173 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top 2174 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { 2175 mPrevSelectedEvent = null; 2176 prevTop = mPrevBox.top; 2177 prevBottom = mPrevBox.bottom; 2178 prevLeft = mPrevBox.left; 2179 prevRight = mPrevBox.right; 2180 } else { 2181 // Clip the top and bottom to the previous selection box. 2182 if (prevTop < mPrevBox.top) { 2183 prevTop = mPrevBox.top; 2184 } 2185 if (prevBottom > mPrevBox.bottom) { 2186 prevBottom = mPrevBox.bottom; 2187 } 2188 } 2189 } else { 2190 // Just use the previously drawn selection box 2191 prevTop = mPrevBox.top; 2192 prevBottom = mPrevBox.bottom; 2193 prevLeft = mPrevBox.left; 2194 prevRight = mPrevBox.right; 2195 } 2196 2197 // Figure out where we came from and compute the center of that area. 2198 if (prevLeft >= box.right) { 2199 // The previously selected event was to the right of us. 2200 prevLocation = FROM_RIGHT; 2201 prevCenter = (prevTop + prevBottom) / 2; 2202 } else if (prevRight <= box.left) { 2203 // The previously selected event was to the left of us. 2204 prevLocation = FROM_LEFT; 2205 prevCenter = (prevTop + prevBottom) / 2; 2206 } else if (prevBottom <= box.top) { 2207 // The previously selected event was above us. 2208 prevLocation = FROM_ABOVE; 2209 prevCenter = (prevLeft + prevRight) / 2; 2210 } else if (prevTop >= box.bottom) { 2211 // The previously selected event was below us. 2212 prevLocation = FROM_BELOW; 2213 prevCenter = (prevLeft + prevRight) / 2; 2214 } 2215 2216 // For each event in the selected event list "mSelectedEvents", search 2217 // all the other events in that list for the nearest neighbor in 4 2218 // directions. 2219 for (int ii = 0; ii < len; ii++) { 2220 Event ev = mSelectedEvents.get(ii); 2221 2222 int startTime = ev.startTime; 2223 int endTime = ev.endTime; 2224 int left = (int) ev.left; 2225 int right = (int) ev.right; 2226 int top = (int) ev.top; 2227 if (top < box.top) { 2228 top = box.top; 2229 } 2230 int bottom = (int) ev.bottom; 2231 if (bottom > box.bottom) { 2232 bottom = box.bottom; 2233 } 2234 if (false) { 2235 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 2236 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 2237 if (DateFormat.is24HourFormat(mParentActivity)) { 2238 flags |= DateUtils.FORMAT_24HOUR; 2239 } 2240 String timeRange = Utils.formatDateRange(mParentActivity, 2241 ev.startMillis, ev.endMillis, flags); 2242 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top 2243 + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title); 2244 } 2245 int upDistanceMin = 10000; // any large number 2246 int downDistanceMin = 10000; // any large number 2247 int leftDistanceMin = 10000; // any large number 2248 int rightDistanceMin = 10000; // any large number 2249 Event upEvent = null; 2250 Event downEvent = null; 2251 Event leftEvent = null; 2252 Event rightEvent = null; 2253 2254 // Pick the starting event closest to the previously selected event, 2255 // if any. distance1 takes precedence over distance2. 2256 int distance1 = 0; 2257 int distance2 = 0; 2258 if (prevLocation == FROM_ABOVE) { 2259 if (left >= prevCenter) { 2260 distance1 = left - prevCenter; 2261 } else if (right <= prevCenter) { 2262 distance1 = prevCenter - right; 2263 } 2264 distance2 = top - prevBottom; 2265 } else if (prevLocation == FROM_BELOW) { 2266 if (left >= prevCenter) { 2267 distance1 = left - prevCenter; 2268 } else if (right <= prevCenter) { 2269 distance1 = prevCenter - right; 2270 } 2271 distance2 = prevTop - bottom; 2272 } else if (prevLocation == FROM_LEFT) { 2273 if (bottom <= prevCenter) { 2274 distance1 = prevCenter - bottom; 2275 } else if (top >= prevCenter) { 2276 distance1 = top - prevCenter; 2277 } 2278 distance2 = left - prevRight; 2279 } else if (prevLocation == FROM_RIGHT) { 2280 if (bottom <= prevCenter) { 2281 distance1 = prevCenter - bottom; 2282 } else if (top >= prevCenter) { 2283 distance1 = top - prevCenter; 2284 } 2285 distance2 = prevLeft - right; 2286 } 2287 if (distance1 < startEventDistance1 2288 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { 2289 startEvent = ev; 2290 startEventDistance1 = distance1; 2291 startEventDistance2 = distance2; 2292 } 2293 2294 // For each neighbor, figure out if it is above or below or left 2295 // or right of me and compute the distance. 2296 for (int jj = 0; jj < len; jj++) { 2297 if (jj == ii) { 2298 continue; 2299 } 2300 Event neighbor = mSelectedEvents.get(jj); 2301 int neighborLeft = (int) neighbor.left; 2302 int neighborRight = (int) neighbor.right; 2303 if (neighbor.endTime <= startTime) { 2304 // This neighbor is entirely above me. 2305 // If we overlap the same column, then compute the distance. 2306 if (neighborLeft < right && neighborRight > left) { 2307 int distance = startTime - neighbor.endTime; 2308 if (distance < upDistanceMin) { 2309 upDistanceMin = distance; 2310 upEvent = neighbor; 2311 } else if (distance == upDistanceMin) { 2312 int center = (left + right) / 2; 2313 int currentDistance = 0; 2314 int currentLeft = (int) upEvent.left; 2315 int currentRight = (int) upEvent.right; 2316 if (currentRight <= center) { 2317 currentDistance = center - currentRight; 2318 } else if (currentLeft >= center) { 2319 currentDistance = currentLeft - center; 2320 } 2321 2322 int neighborDistance = 0; 2323 if (neighborRight <= center) { 2324 neighborDistance = center - neighborRight; 2325 } else if (neighborLeft >= center) { 2326 neighborDistance = neighborLeft - center; 2327 } 2328 if (neighborDistance < currentDistance) { 2329 upDistanceMin = distance; 2330 upEvent = neighbor; 2331 } 2332 } 2333 } 2334 } else if (neighbor.startTime >= endTime) { 2335 // This neighbor is entirely below me. 2336 // If we overlap the same column, then compute the distance. 2337 if (neighborLeft < right && neighborRight > left) { 2338 int distance = neighbor.startTime - endTime; 2339 if (distance < downDistanceMin) { 2340 downDistanceMin = distance; 2341 downEvent = neighbor; 2342 } else if (distance == downDistanceMin) { 2343 int center = (left + right) / 2; 2344 int currentDistance = 0; 2345 int currentLeft = (int) downEvent.left; 2346 int currentRight = (int) downEvent.right; 2347 if (currentRight <= center) { 2348 currentDistance = center - currentRight; 2349 } else if (currentLeft >= center) { 2350 currentDistance = currentLeft - center; 2351 } 2352 2353 int neighborDistance = 0; 2354 if (neighborRight <= center) { 2355 neighborDistance = center - neighborRight; 2356 } else if (neighborLeft >= center) { 2357 neighborDistance = neighborLeft - center; 2358 } 2359 if (neighborDistance < currentDistance) { 2360 downDistanceMin = distance; 2361 downEvent = neighbor; 2362 } 2363 } 2364 } 2365 } 2366 2367 if (neighborLeft >= right) { 2368 // This neighbor is entirely to the right of me. 2369 // Take the closest neighbor in the y direction. 2370 int center = (top + bottom) / 2; 2371 int distance = 0; 2372 int neighborBottom = (int) neighbor.bottom; 2373 int neighborTop = (int) neighbor.top; 2374 if (neighborBottom <= center) { 2375 distance = center - neighborBottom; 2376 } else if (neighborTop >= center) { 2377 distance = neighborTop - center; 2378 } 2379 if (distance < rightDistanceMin) { 2380 rightDistanceMin = distance; 2381 rightEvent = neighbor; 2382 } else if (distance == rightDistanceMin) { 2383 // Pick the closest in the x direction 2384 int neighborDistance = neighborLeft - right; 2385 int currentDistance = (int) rightEvent.left - right; 2386 if (neighborDistance < currentDistance) { 2387 rightDistanceMin = distance; 2388 rightEvent = neighbor; 2389 } 2390 } 2391 } else if (neighborRight <= left) { 2392 // This neighbor is entirely to the left of me. 2393 // Take the closest neighbor in the y direction. 2394 int center = (top + bottom) / 2; 2395 int distance = 0; 2396 int neighborBottom = (int) neighbor.bottom; 2397 int neighborTop = (int) neighbor.top; 2398 if (neighborBottom <= center) { 2399 distance = center - neighborBottom; 2400 } else if (neighborTop >= center) { 2401 distance = neighborTop - center; 2402 } 2403 if (distance < leftDistanceMin) { 2404 leftDistanceMin = distance; 2405 leftEvent = neighbor; 2406 } else if (distance == leftDistanceMin) { 2407 // Pick the closest in the x direction 2408 int neighborDistance = left - neighborRight; 2409 int currentDistance = left - (int) leftEvent.right; 2410 if (neighborDistance < currentDistance) { 2411 leftDistanceMin = distance; 2412 leftEvent = neighbor; 2413 } 2414 } 2415 } 2416 } 2417 ev.nextUp = upEvent; 2418 ev.nextDown = downEvent; 2419 ev.nextLeft = leftEvent; 2420 ev.nextRight = rightEvent; 2421 } 2422 mSelectedEvent = startEvent; 2423 } 2424 2425 2426 private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) { 2427 2428 int color = event.color; 2429 2430 // Fade visible boxes if event was declined. 2431 boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED); 2432 if (declined) { 2433 int alpha = color & 0xff000000; 2434 color &= 0x00ffffff; 2435 int red = (color & 0x00ff0000) >> 16; 2436 int green = (color & 0x0000ff00) >> 8; 2437 int blue = (color & 0x0000ff); 2438 color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1); 2439 color += 0x7F7F7F + alpha; 2440 } 2441 2442 // If this event is selected, then use the selection color 2443 if (mSelectedEvent == event) { 2444 if (mSelectionMode == SELECTION_PRESSED || mSelectionMode == SELECTION_SELECTED) { 2445 // Also, remember the last selected event that we drew 2446 mPrevSelectedEvent = event; 2447 p.setColor(mSelectionColor); 2448 eventTextPaint.setColor(mSelectedEventTextColor); 2449 } else if (mSelectionMode == SELECTION_LONGPRESS) { 2450 p.setColor(mSelectionColor); 2451 eventTextPaint.setColor(mSelectedEventTextColor); 2452 } else { 2453 p.setColor(color); 2454 eventTextPaint.setColor(mEventTextColor); 2455 } 2456 } else { 2457 p.setColor(color); 2458 eventTextPaint.setColor(mEventTextColor); 2459 } 2460 2461 2462 RectF rf = mRectF; 2463 rf.top = event.top; 2464 rf.bottom = event.bottom; 2465 rf.left = event.left; 2466 rf.right = event.right - 1; 2467 2468 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p); 2469 2470 // Draw a darker border 2471 float[] hsv = new float[3]; 2472 Color.colorToHSV(p.getColor(), hsv); 2473 hsv[1] = 1.0f; 2474 hsv[2] *= 0.75f; 2475 mPaintBorder.setColor(Color.HSVToColor(hsv)); 2476 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder); 2477 2478 rf.left += 2; 2479 rf.right -= 2; 2480 2481 return rf; 2482 } 2483 2484 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); 2485 2486 // Sanitize a string before passing it to drawText or else we get little 2487 // squares. For newlines and tabs before a comma, delete the character. 2488 // Otherwise, just replace them with a space. 2489 private String drawTextSanitizer(String string) { 2490 Matcher m = drawTextSanitizerFilter.matcher(string); 2491 string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' '); 2492 return string; 2493 } 2494 2495 private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) { 2496 if (!mDrawTextInEventRect) { 2497 return; 2498 } 2499 2500 float width = rf.right - rf.left; 2501 float height = rf.bottom - rf.top; 2502 2503 // Leave one pixel extra space between lines 2504 int lineHeight = mEventTextHeight + 1; 2505 2506 // If the rectangle is too small for text, then return 2507 if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) { 2508 return; 2509 } 2510 2511 // Truncate the event title to a known (large enough) limit 2512 String text = event.getTitleAndLocation(); 2513 2514 text = drawTextSanitizer(text); 2515 2516 int len = text.length(); 2517 if (len > MAX_EVENT_TEXT_LEN) { 2518 text = text.substring(0, MAX_EVENT_TEXT_LEN); 2519 len = MAX_EVENT_TEXT_LEN; 2520 } 2521 2522 // Figure out how much space the event title will take, and create a 2523 // String fragment that will fit in the rectangle. Use multiple lines, 2524 // if available. 2525 p.getTextWidths(text, mCharWidths); 2526 String fragment = text; 2527 float top = rf.top + mEventTextAscent + topMargin; 2528 int start = 0; 2529 2530 // Leave one pixel extra space at the bottom 2531 while (start < len && height >= (lineHeight + 1)) { 2532 boolean lastLine = (height < 2 * lineHeight + 1); 2533 // Skip leading spaces at the beginning of each line 2534 do { 2535 char c = text.charAt(start); 2536 if (c != ' ') break; 2537 start += 1; 2538 } while (start < len); 2539 2540 float sum = 0; 2541 int end = start; 2542 for (int ii = start; ii < len; ii++) { 2543 char c = text.charAt(ii); 2544 2545 // If we found the end of a word, then remember the ending 2546 // position. 2547 if (c == ' ') { 2548 end = ii; 2549 } 2550 sum += mCharWidths[ii]; 2551 // If adding this character would exceed the width and this 2552 // isn't the last line, then break the line at the previous 2553 // word. If there was no previous word, then break this word. 2554 if (sum > width) { 2555 if (end > start && !lastLine) { 2556 // There was a previous word on this line. 2557 fragment = text.substring(start, end); 2558 start = end; 2559 break; 2560 } 2561 2562 // This is the only word and it is too long to fit on 2563 // the line (or this is the last line), so take as many 2564 // characters of this word as will fit. 2565 fragment = text.substring(start, ii); 2566 start = ii; 2567 break; 2568 } 2569 } 2570 2571 // If sum <= width, then we can fit the rest of the text on 2572 // this line. 2573 if (sum <= width) { 2574 fragment = text.substring(start, len); 2575 start = len; 2576 } 2577 2578 canvas.drawText(fragment, rf.left + 1, top, p); 2579 2580 top += lineHeight; 2581 height -= lineHeight; 2582 } 2583 } 2584 2585 private void updateEventDetails() { 2586 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN 2587 || mSelectionMode == SELECTION_LONGPRESS) { 2588 mPopup.dismiss(); 2589 return; 2590 } 2591 if (mLastPopupEventID == mSelectedEvent.id) { 2592 return; 2593 } 2594 2595 mLastPopupEventID = mSelectedEvent.id; 2596 2597 // Remove any outstanding callbacks to dismiss the popup. 2598 getHandler().removeCallbacks(mDismissPopup); 2599 2600 Event event = mSelectedEvent; 2601 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); 2602 titleView.setText(event.title); 2603 2604 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); 2605 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); 2606 2607 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); 2608 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); 2609 2610 int flags; 2611 if (event.allDay) { 2612 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE | 2613 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; 2614 } else { 2615 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 2616 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL 2617 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 2618 } 2619 if (DateFormat.is24HourFormat(mParentActivity)) { 2620 flags |= DateUtils.FORMAT_24HOUR; 2621 } 2622 String timeRange = Utils.formatDateRange(mParentActivity, 2623 event.startMillis, event.endMillis, flags); 2624 TextView timeView = (TextView) mPopupView.findViewById(R.id.time); 2625 timeView.setText(timeRange); 2626 2627 TextView whereView = (TextView) mPopupView.findViewById(R.id.where); 2628 final boolean empty = TextUtils.isEmpty(event.location); 2629 whereView.setVisibility(empty ? View.GONE : View.VISIBLE); 2630 if (!empty) whereView.setText(event.location); 2631 2632 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); 2633 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); 2634 2635 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 2636 } 2637 2638 // The following routines are called from the parent activity when certain 2639 // touch events occur. 2640 2641 void doDown(MotionEvent ev) { 2642 mTouchMode = TOUCH_MODE_DOWN; 2643 mViewStartX = 0; 2644 mOnFlingCalled = false; 2645 getHandler().removeCallbacks(mContinueScroll); 2646 } 2647 2648 void doSingleTapUp(MotionEvent ev) { 2649 int x = (int) ev.getX(); 2650 int y = (int) ev.getY(); 2651 int selectedDay = mSelectionDay; 2652 int selectedHour = mSelectionHour; 2653 2654 boolean validPosition = setSelectionFromPosition(x, y); 2655 if (!validPosition) { 2656 // return if the touch wasn't on an area of concern 2657 return; 2658 } 2659 2660 mSelectionMode = SELECTION_SELECTED; 2661 mRedrawScreen = true; 2662 invalidate(); 2663 2664 boolean launchNewView = false; 2665 if (mSelectedEvent != null) { 2666 // If the tap is on an event, launch the "View event" view 2667 launchNewView = true; 2668 } else if (mSelectedEvent == null && selectedDay == mSelectionDay 2669 && selectedHour == mSelectionHour) { 2670 // If the tap is on an already selected hour slot, 2671 // then launch the Day/Agenda view. Otherwise, just select the hour 2672 // slot. 2673 launchNewView = true; 2674 } 2675 2676 if (launchNewView) { 2677 switchViews(false /* not the trackball */); 2678 } 2679 } 2680 2681 void doLongPress(MotionEvent ev) { 2682 int x = (int) ev.getX(); 2683 int y = (int) ev.getY(); 2684 2685 boolean validPosition = setSelectionFromPosition(x, y); 2686 if (!validPosition) { 2687 // return if the touch wasn't on an area of concern 2688 return; 2689 } 2690 2691 mSelectionMode = SELECTION_LONGPRESS; 2692 mRedrawScreen = true; 2693 invalidate(); 2694 performLongClick(); 2695 } 2696 2697 void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { 2698 // Use the distance from the current point to the initial touch instead 2699 // of deltaX and deltaY to avoid accumulating floating-point rounding 2700 // errors. Also, we don't need floats, we can use ints. 2701 int distanceX = (int) e1.getX() - (int) e2.getX(); 2702 int distanceY = (int) e1.getY() - (int) e2.getY(); 2703 2704 // If we haven't figured out the predominant scroll direction yet, 2705 // then do it now. 2706 if (mTouchMode == TOUCH_MODE_DOWN) { 2707 int absDistanceX = Math.abs(distanceX); 2708 int absDistanceY = Math.abs(distanceY); 2709 mScrollStartY = mViewStartY; 2710 mPreviousDistanceX = 0; 2711 mPreviousDirection = 0; 2712 2713 // If the x distance is at least twice the y distance, then lock 2714 // the scroll horizontally. Otherwise scroll vertically. 2715 if (absDistanceX >= 2 * absDistanceY) { 2716 mTouchMode = TOUCH_MODE_HSCROLL; 2717 mViewStartX = distanceX; 2718 initNextView(-mViewStartX); 2719 } else { 2720 mTouchMode = TOUCH_MODE_VSCROLL; 2721 } 2722 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 2723 // We are already scrolling horizontally, so check if we 2724 // changed the direction of scrolling so that the other week 2725 // is now visible. 2726 mViewStartX = distanceX; 2727 if (distanceX != 0) { 2728 int direction = (distanceX > 0) ? 1 : -1; 2729 if (direction != mPreviousDirection) { 2730 // The user has switched the direction of scrolling 2731 // so re-init the next view 2732 initNextView(-mViewStartX); 2733 mPreviousDirection = direction; 2734 } 2735 } 2736 2737 // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD, 2738 // then change the title to the new day (or week), but only 2739 // if we haven't already changed the title. 2740 if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) { 2741 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) { 2742 CalendarView view = mParentActivity.getNextView(); 2743 mTitleTextView.setText(view.mDateRange); 2744 } 2745 } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) { 2746 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) { 2747 CalendarView view = mParentActivity.getNextView(); 2748 mTitleTextView.setText(view.mDateRange); 2749 } 2750 } else { 2751 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD 2752 || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) { 2753 mTitleTextView.setText(mDateRange); 2754 } 2755 } 2756 mPreviousDistanceX = distanceX; 2757 } 2758 2759 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { 2760 mViewStartY = mScrollStartY + distanceY; 2761 if (mViewStartY < 0) { 2762 mViewStartY = 0; 2763 } else if (mViewStartY > mMaxViewStartY) { 2764 mViewStartY = mMaxViewStartY; 2765 } 2766 computeFirstHour(); 2767 } 2768 2769 mScrolling = true; 2770 2771 if (mSelectionMode != SELECTION_HIDDEN) { 2772 mSelectionMode = SELECTION_HIDDEN; 2773 mRedrawScreen = true; 2774 } 2775 invalidate(); 2776 } 2777 2778 void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 2779 mTouchMode = TOUCH_MODE_INITIAL_STATE; 2780 mSelectionMode = SELECTION_HIDDEN; 2781 mOnFlingCalled = true; 2782 int deltaX = (int) e2.getX() - (int) e1.getX(); 2783 int distanceX = Math.abs(deltaX); 2784 int deltaY = (int) e2.getY() - (int) e1.getY(); 2785 int distanceY = Math.abs(deltaY); 2786 2787 if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) { 2788 boolean switchForward = initNextView(deltaX); 2789 CalendarView view = mParentActivity.getNextView(); 2790 mTitleTextView.setText(view.mDateRange); 2791 mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth); 2792 mViewStartX = 0; 2793 return; 2794 } 2795 2796 // Continue scrolling vertically 2797 mContinueScroll.init((int) velocityY / 20); 2798 post(mContinueScroll); 2799 } 2800 2801 private boolean initNextView(int deltaX) { 2802 // Change the view to the previous day or week 2803 CalendarView view = mParentActivity.getNextView(); 2804 Time date = view.mBaseDate; 2805 date.set(mBaseDate); 2806 boolean switchForward; 2807 if (deltaX > 0) { 2808 date.monthDay -= mNumDays; 2809 view.mSelectionDay = mSelectionDay - mNumDays; 2810 switchForward = false; 2811 } else { 2812 date.monthDay += mNumDays; 2813 view.mSelectionDay = mSelectionDay + mNumDays; 2814 switchForward = true; 2815 } 2816 date.normalize(true /* ignore isDst */); 2817 initView(view); 2818 view.layout(getLeft(), getTop(), getRight(), getBottom()); 2819 view.reloadEvents(); 2820 return switchForward; 2821 } 2822 2823 @Override 2824 public boolean onTouchEvent(MotionEvent ev) { 2825 int action = ev.getAction(); 2826 2827 switch (action) { 2828 case MotionEvent.ACTION_DOWN: 2829 mParentActivity.mGestureDetector.onTouchEvent(ev); 2830 return true; 2831 2832 case MotionEvent.ACTION_MOVE: 2833 mParentActivity.mGestureDetector.onTouchEvent(ev); 2834 return true; 2835 2836 case MotionEvent.ACTION_UP: 2837 mParentActivity.mGestureDetector.onTouchEvent(ev); 2838 if (mOnFlingCalled) { 2839 return true; 2840 } 2841 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 2842 mTouchMode = TOUCH_MODE_INITIAL_STATE; 2843 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) { 2844 // The user has gone beyond the threshold so switch views 2845 mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth); 2846 mViewStartX = 0; 2847 return true; 2848 } else { 2849 // Not beyond the threshold so invalidate which will cause 2850 // the view to snap back. Also call recalc() to ensure 2851 // that we have the correct starting date and title. 2852 recalc(); 2853 mTitleTextView.setText(mDateRange); 2854 invalidate(); 2855 mViewStartX = 0; 2856 } 2857 } 2858 2859 // If we were scrolling, then reset the selected hour so that it 2860 // is visible. 2861 if (mScrolling) { 2862 mScrolling = false; 2863 resetSelectedHour(); 2864 mRedrawScreen = true; 2865 invalidate(); 2866 } 2867 return true; 2868 2869 // This case isn't expected to happen. 2870 case MotionEvent.ACTION_CANCEL: 2871 mParentActivity.mGestureDetector.onTouchEvent(ev); 2872 mScrolling = false; 2873 resetSelectedHour(); 2874 return true; 2875 2876 default: 2877 if (mParentActivity.mGestureDetector.onTouchEvent(ev)) { 2878 return true; 2879 } 2880 return super.onTouchEvent(ev); 2881 } 2882 } 2883 2884 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 2885 MenuItem item; 2886 2887 // If the trackball is held down, then the context menu pops up and 2888 // we never get onKeyUp() for the long-press. So check for it here 2889 // and change the selection to the long-press state. 2890 if (mSelectionMode != SELECTION_LONGPRESS) { 2891 mSelectionMode = SELECTION_LONGPRESS; 2892 mRedrawScreen = true; 2893 invalidate(); 2894 } 2895 2896 final long startMillis = getSelectedTimeInMillis(); 2897 int flags = DateUtils.FORMAT_SHOW_TIME 2898 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT 2899 | DateUtils.FORMAT_SHOW_WEEKDAY; 2900 final String title = Utils.formatDateRange(mParentActivity, startMillis, startMillis, 2901 flags); 2902 menu.setHeaderTitle(title); 2903 2904 int numSelectedEvents = mSelectedEvents.size(); 2905 if (mNumDays == 1) { 2906 // Day view. 2907 2908 // If there is a selected event, then allow it to be viewed and 2909 // edited. 2910 if (numSelectedEvents >= 1) { 2911 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view); 2912 item.setOnMenuItemClickListener(mContextMenuHandler); 2913 item.setIcon(android.R.drawable.ic_menu_info_details); 2914 2915 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent); 2916 if (accessLevel == ACCESS_LEVEL_EDIT) { 2917 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit); 2918 item.setOnMenuItemClickListener(mContextMenuHandler); 2919 item.setIcon(android.R.drawable.ic_menu_edit); 2920 item.setAlphabeticShortcut('e'); 2921 } 2922 2923 if (accessLevel >= ACCESS_LEVEL_DELETE) { 2924 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete); 2925 item.setOnMenuItemClickListener(mContextMenuHandler); 2926 item.setIcon(android.R.drawable.ic_menu_delete); 2927 } 2928 2929 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create); 2930 item.setOnMenuItemClickListener(mContextMenuHandler); 2931 item.setIcon(android.R.drawable.ic_menu_add); 2932 item.setAlphabeticShortcut('n'); 2933 } else { 2934 // Otherwise, if the user long-pressed on a blank hour, allow 2935 // them to create an event. They can also do this by tapping. 2936 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create); 2937 item.setOnMenuItemClickListener(mContextMenuHandler); 2938 item.setIcon(android.R.drawable.ic_menu_add); 2939 item.setAlphabeticShortcut('n'); 2940 } 2941 } else { 2942 // Week view. 2943 2944 // If there is a selected event, then allow it to be viewed and 2945 // edited. 2946 if (numSelectedEvents >= 1) { 2947 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view); 2948 item.setOnMenuItemClickListener(mContextMenuHandler); 2949 item.setIcon(android.R.drawable.ic_menu_info_details); 2950 2951 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent); 2952 if (accessLevel == ACCESS_LEVEL_EDIT) { 2953 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit); 2954 item.setOnMenuItemClickListener(mContextMenuHandler); 2955 item.setIcon(android.R.drawable.ic_menu_edit); 2956 item.setAlphabeticShortcut('e'); 2957 } 2958 2959 if (accessLevel >= ACCESS_LEVEL_DELETE) { 2960 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete); 2961 item.setOnMenuItemClickListener(mContextMenuHandler); 2962 item.setIcon(android.R.drawable.ic_menu_delete); 2963 } 2964 2965 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create); 2966 item.setOnMenuItemClickListener(mContextMenuHandler); 2967 item.setIcon(android.R.drawable.ic_menu_add); 2968 item.setAlphabeticShortcut('n'); 2969 2970 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view); 2971 item.setOnMenuItemClickListener(mContextMenuHandler); 2972 item.setIcon(android.R.drawable.ic_menu_day); 2973 item.setAlphabeticShortcut('d'); 2974 2975 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view); 2976 item.setOnMenuItemClickListener(mContextMenuHandler); 2977 item.setIcon(android.R.drawable.ic_menu_agenda); 2978 item.setAlphabeticShortcut('a'); 2979 } else { 2980 // No events are selected 2981 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create); 2982 item.setOnMenuItemClickListener(mContextMenuHandler); 2983 item.setIcon(android.R.drawable.ic_menu_add); 2984 item.setAlphabeticShortcut('n'); 2985 2986 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view); 2987 item.setOnMenuItemClickListener(mContextMenuHandler); 2988 item.setIcon(android.R.drawable.ic_menu_day); 2989 item.setAlphabeticShortcut('d'); 2990 2991 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view); 2992 item.setOnMenuItemClickListener(mContextMenuHandler); 2993 item.setIcon(android.R.drawable.ic_menu_agenda); 2994 item.setAlphabeticShortcut('a'); 2995 } 2996 } 2997 2998 mPopup.dismiss(); 2999 } 3000 3001 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { 3002 public boolean onMenuItemClick(MenuItem item) { 3003 switch (item.getItemId()) { 3004 case MenuHelper.MENU_EVENT_VIEW: { 3005 if (mSelectedEvent != null) { 3006 long id = mSelectedEvent.id; 3007 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 3008 Intent intent = new Intent(Intent.ACTION_VIEW); 3009 intent.setData(eventUri); 3010 intent.setClassName(mParentActivity, EventInfoActivity.class.getName()); 3011 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis); 3012 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis); 3013 mParentActivity.startActivity(intent); 3014 } 3015 break; 3016 } 3017 case MenuHelper.MENU_EVENT_EDIT: { 3018 if (mSelectedEvent != null) { 3019 long id = mSelectedEvent.id; 3020 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 3021 Intent intent = new Intent(Intent.ACTION_EDIT); 3022 intent.setData(eventUri); 3023 intent.setClassName(mParentActivity, EditEvent.class.getName()); 3024 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis); 3025 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis); 3026 mParentActivity.startActivity(intent); 3027 } 3028 break; 3029 } 3030 case MenuHelper.MENU_DAY: { 3031 long startMillis = getSelectedTimeInMillis(); 3032 Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis); 3033 break; 3034 } 3035 case MenuHelper.MENU_AGENDA: { 3036 long startMillis = getSelectedTimeInMillis(); 3037 Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis); 3038 break; 3039 } 3040 case MenuHelper.MENU_EVENT_CREATE: { 3041 long startMillis = getSelectedTimeInMillis(); 3042 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 3043 Intent intent = new Intent(Intent.ACTION_VIEW); 3044 intent.setClassName(mParentActivity, EditEvent.class.getName()); 3045 intent.putExtra(EVENT_BEGIN_TIME, startMillis); 3046 intent.putExtra(EVENT_END_TIME, endMillis); 3047 intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay); 3048 mParentActivity.startActivity(intent); 3049 break; 3050 } 3051 case MenuHelper.MENU_EVENT_DELETE: { 3052 if (mSelectedEvent != null) { 3053 Event selectedEvent = mSelectedEvent; 3054 long begin = selectedEvent.startMillis; 3055 long end = selectedEvent.endMillis; 3056 long id = selectedEvent.id; 3057 mDeleteEventHelper.delete(begin, end, id, -1); 3058 } 3059 break; 3060 } 3061 default: { 3062 return false; 3063 } 3064 } 3065 return true; 3066 } 3067 } 3068 3069 private static int getEventAccessLevel(Context context, Event e) { 3070 ContentResolver cr = context.getContentResolver(); 3071 3072 int visibility = Calendars.NO_ACCESS; 3073 int relationship = Attendees.RELATIONSHIP_ORGANIZER; 3074 3075 // Get the calendar id for this event 3076 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id), 3077 new String[] { Events.CALENDAR_ID }, 3078 null /* selection */, 3079 null /* selectionArgs */, 3080 null /* sort */); 3081 3082 if (cursor == null) { 3083 return ACCESS_LEVEL_NONE; 3084 } 3085 3086 if (cursor.getCount() == 0) { 3087 cursor.close(); 3088 return ACCESS_LEVEL_NONE; 3089 } 3090 3091 cursor.moveToFirst(); 3092 long calId = cursor.getLong(0); 3093 cursor.close(); 3094 3095 Uri uri = Calendars.CONTENT_URI; 3096 String where = String.format(CALENDARS_WHERE, calId); 3097 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null); 3098 3099 String calendarOwnerAccount = null; 3100 if (cursor != null) { 3101 cursor.moveToFirst(); 3102 visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); 3103 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 3104 cursor.close(); 3105 } 3106 3107 if (visibility < Calendars.CONTRIBUTOR_ACCESS) { 3108 return ACCESS_LEVEL_NONE; 3109 } 3110 3111 if (e.guestsCanModify) { 3112 return ACCESS_LEVEL_EDIT; 3113 } 3114 3115 if (!TextUtils.isEmpty(calendarOwnerAccount) && 3116 calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { 3117 return ACCESS_LEVEL_EDIT; 3118 } 3119 3120 return ACCESS_LEVEL_DELETE; 3121 } 3122 3123 /** 3124 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. 3125 * If the touch position is not within the displayed grid, then this 3126 * method returns false. 3127 * 3128 * @param x the x position of the touch 3129 * @param y the y position of the touch 3130 * @return true if the touch position is valid 3131 */ 3132 private boolean setSelectionFromPosition(int x, int y) { 3133 if (x < mHoursWidth) { 3134 return false; 3135 } 3136 3137 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); 3138 if (day >= mNumDays) { 3139 day = mNumDays - 1; 3140 } 3141 day += mFirstJulianDay; 3142 int hour; 3143 if (y < mFirstCell + mFirstHourOffset) { 3144 mSelectionAllDay = true; 3145 } else { 3146 hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP); 3147 hour += mFirstHour; 3148 mSelectionHour = hour; 3149 mSelectionAllDay = false; 3150 } 3151 mSelectionDay = day; 3152 findSelectedEvent(x, y); 3153 // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day 3154 // + " hour: " + hour 3155 // + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset); 3156 // if (mSelectedEvent != null) { 3157 // Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title); 3158 // for (Event ev : mSelectedEvents) { 3159 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 3160 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3161 // String timeRange = Utils.formatDateRange(mParentActivity, 3162 // ev.startMillis, ev.endMillis, flags); 3163 // 3164 // Log.i("Cal", " " + timeRange + " " + ev.title); 3165 // } 3166 // } 3167 return true; 3168 } 3169 3170 private void findSelectedEvent(int x, int y) { 3171 int date = mSelectionDay; 3172 int cellWidth = mCellWidth; 3173 ArrayList<Event> events = mEvents; 3174 int numEvents = events.size(); 3175 int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP); 3176 int top = 0; 3177 mSelectedEvent = null; 3178 3179 mSelectedEvents.clear(); 3180 if (mSelectionAllDay) { 3181 float yDistance; 3182 float minYdistance = 10000.0f; // any large number 3183 Event closestEvent = null; 3184 float drawHeight = mAllDayHeight; 3185 int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN; 3186 for (int i = 0; i < numEvents; i++) { 3187 Event event = events.get(i); 3188 if (!event.allDay) { 3189 continue; 3190 } 3191 3192 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { 3193 float numRectangles = event.getMaxColumns(); 3194 float height = drawHeight / numRectangles; 3195 if (height > MAX_ALLDAY_EVENT_HEIGHT) { 3196 height = MAX_ALLDAY_EVENT_HEIGHT; 3197 } 3198 float eventTop = yOffset + height * event.getColumn(); 3199 float eventBottom = eventTop + height; 3200 if (eventTop < y && eventBottom > y) { 3201 // If the touch is inside the event rectangle, then 3202 // add the event. 3203 mSelectedEvents.add(event); 3204 closestEvent = event; 3205 break; 3206 } else { 3207 // Find the closest event 3208 if (eventTop >= y) { 3209 yDistance = eventTop - y; 3210 } else { 3211 yDistance = y - eventBottom; 3212 } 3213 if (yDistance < minYdistance) { 3214 minYdistance = yDistance; 3215 closestEvent = event; 3216 } 3217 } 3218 } 3219 } 3220 mSelectedEvent = closestEvent; 3221 return; 3222 } 3223 3224 // Adjust y for the scrollable bitmap 3225 y += mViewStartY - mFirstCell; 3226 3227 // Use a region around (x,y) for the selection region 3228 Rect region = mRect; 3229 region.left = x - 10; 3230 region.right = x + 10; 3231 region.top = y - 10; 3232 region.bottom = y + 10; 3233 3234 EventGeometry geometry = mEventGeometry; 3235 3236 for (int i = 0; i < numEvents; i++) { 3237 Event event = events.get(i); 3238 // Compute the event rectangle. 3239 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 3240 continue; 3241 } 3242 3243 // If the event intersects the selection region, then add it to 3244 // mSelectedEvents. 3245 if (geometry.eventIntersectsSelection(event, region)) { 3246 mSelectedEvents.add(event); 3247 } 3248 } 3249 3250 // If there are any events in the selected region, then assign the 3251 // closest one to mSelectedEvent. 3252 if (mSelectedEvents.size() > 0) { 3253 int len = mSelectedEvents.size(); 3254 Event closestEvent = null; 3255 float minDist = mViewWidth + mViewHeight; // some large distance 3256 for (int index = 0; index < len; index++) { 3257 Event ev = mSelectedEvents.get(index); 3258 float dist = geometry.pointToEvent(x, y, ev); 3259 if (dist < minDist) { 3260 minDist = dist; 3261 closestEvent = ev; 3262 } 3263 } 3264 mSelectedEvent = closestEvent; 3265 3266 // Keep the selected hour and day consistent with the selected 3267 // event. They could be different if we touched on an empty hour 3268 // slot very close to an event in the previous hour slot. In 3269 // that case we will select the nearby event. 3270 int startDay = mSelectedEvent.startDay; 3271 int endDay = mSelectedEvent.endDay; 3272 if (mSelectionDay < startDay) { 3273 mSelectionDay = startDay; 3274 } else if (mSelectionDay > endDay) { 3275 mSelectionDay = endDay; 3276 } 3277 3278 int startHour = mSelectedEvent.startTime / 60; 3279 int endHour; 3280 if (mSelectedEvent.startTime < mSelectedEvent.endTime) { 3281 endHour = (mSelectedEvent.endTime - 1) / 60; 3282 } else { 3283 endHour = mSelectedEvent.endTime / 60; 3284 } 3285 3286 if (mSelectionHour < startHour) { 3287 mSelectionHour = startHour; 3288 } else if (mSelectionHour > endHour) { 3289 mSelectionHour = endHour; 3290 } 3291 } 3292 } 3293 3294 // Encapsulates the code to continue the scrolling after the 3295 // finger is lifted. Instead of stopping the scroll immediately, 3296 // the scroll continues to "free spin" and gradually slows down. 3297 private class ContinueScroll implements Runnable { 3298 int mSignDeltaY; 3299 int mAbsDeltaY; 3300 float mFloatDeltaY; 3301 long mFreeSpinTime; 3302 private static final float FRICTION_COEF = 0.7F; 3303 private static final long FREE_SPIN_MILLIS = 180; 3304 private static final int MAX_DELTA = 60; 3305 private static final int SCROLL_REPEAT_INTERVAL = 30; 3306 3307 public void init(int deltaY) { 3308 mSignDeltaY = 0; 3309 if (deltaY > 0) { 3310 mSignDeltaY = 1; 3311 } else if (deltaY < 0) { 3312 mSignDeltaY = -1; 3313 } 3314 mAbsDeltaY = Math.abs(deltaY); 3315 3316 // Limit the maximum speed 3317 if (mAbsDeltaY > MAX_DELTA) { 3318 mAbsDeltaY = MAX_DELTA; 3319 } 3320 mFloatDeltaY = mAbsDeltaY; 3321 mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS; 3322 // Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY 3323 // + " mViewStartY: " + mViewStartY); 3324 } 3325 3326 public void run() { 3327 long time = System.currentTimeMillis(); 3328 3329 // Start out with a frictionless "free spin" 3330 if (time > mFreeSpinTime) { 3331 // If the delta is small, then apply a fixed deceleration. 3332 // Otherwise 3333 if (mAbsDeltaY <= 10) { 3334 mAbsDeltaY -= 2; 3335 } else { 3336 mFloatDeltaY *= FRICTION_COEF; 3337 mAbsDeltaY = (int) mFloatDeltaY; 3338 } 3339 3340 if (mAbsDeltaY < 0) { 3341 mAbsDeltaY = 0; 3342 } 3343 } 3344 3345 if (mSignDeltaY == 1) { 3346 mViewStartY -= mAbsDeltaY; 3347 } else { 3348 mViewStartY += mAbsDeltaY; 3349 } 3350 // Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY 3351 // + " mViewStartY: " + mViewStartY); 3352 3353 if (mViewStartY < 0) { 3354 mViewStartY = 0; 3355 mAbsDeltaY = 0; 3356 } else if (mViewStartY > mMaxViewStartY) { 3357 mViewStartY = mMaxViewStartY; 3358 mAbsDeltaY = 0; 3359 } 3360 3361 computeFirstHour(); 3362 3363 if (mAbsDeltaY > 0) { 3364 postDelayed(this, SCROLL_REPEAT_INTERVAL); 3365 } else { 3366 // Done scrolling. 3367 mScrolling = false; 3368 resetSelectedHour(); 3369 mRedrawScreen = true; 3370 } 3371 3372 invalidate(); 3373 } 3374 } 3375 3376 /** 3377 * Cleanup the pop-up and timers. 3378 */ 3379 public void cleanup() { 3380 // Protect against null-pointer exceptions 3381 if (mPopup != null) { 3382 mPopup.dismiss(); 3383 } 3384 mLastPopupEventID = INVALID_EVENT_ID; 3385 Handler handler = getHandler(); 3386 if (handler != null) { 3387 handler.removeCallbacks(mDismissPopup); 3388 handler.removeCallbacks(mUpdateCurrentTime); 3389 } 3390 3391 // Turn off redraw 3392 mRemeasure = false; 3393 mRedrawScreen = false; 3394 3395 // clear the cached values for accessibility support 3396 mPrevSelectionDay = 0; 3397 mPrevSelectionHour = 0; 3398 mPrevTitleTextViewText = null; 3399 } 3400 3401 /** 3402 * Restart the update timer 3403 */ 3404 public void updateView() { 3405 mUpdateTZ.run(); 3406 post(mUpdateCurrentTime); 3407 } 3408 3409 @Override protected void onDetachedFromWindow() { 3410 cleanup(); 3411 if (mBitmap != null) { 3412 mBitmap.recycle(); 3413 mBitmap = null; 3414 } 3415 super.onDetachedFromWindow(); 3416 } 3417 3418 class DismissPopup implements Runnable { 3419 public void run() { 3420 // Protect against null-pointer exceptions 3421 if (mPopup != null) { 3422 mPopup.dismiss(); 3423 } 3424 } 3425 } 3426 3427 class UpdateCurrentTime implements Runnable { 3428 public void run() { 3429 long currentTime = System.currentTimeMillis(); 3430 mCurrentTime.set(currentTime); 3431 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) 3432 postDelayed(mUpdateCurrentTime, 3433 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 3434 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 3435 mRedrawScreen = true; 3436 invalidate(); 3437 } 3438 } 3439 } 3440