DayView.java revision 3f6328f579b05c02606572c777541b97aa04f9f3
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 17package com.android.calendar; 18 19import com.android.calendar.CalendarController.EventType; 20import com.android.calendar.CalendarController.ViewType; 21 22import android.animation.Animator; 23import android.animation.AnimatorListenerAdapter; 24import android.animation.ObjectAnimator; 25import android.animation.TypeEvaluator; 26import android.animation.ValueAnimator; 27import android.content.ContentResolver; 28import android.content.ContentUris; 29import android.content.Context; 30import android.content.res.Resources; 31import android.content.res.TypedArray; 32import android.database.Cursor; 33import android.graphics.Canvas; 34import android.graphics.Paint; 35import android.graphics.Paint.Style; 36import android.graphics.Rect; 37import android.graphics.Typeface; 38import android.graphics.drawable.Drawable; 39import android.net.Uri; 40import android.os.Handler; 41import android.provider.Calendar.Attendees; 42import android.provider.Calendar.Calendars; 43import android.provider.Calendar.Events; 44import android.text.Layout.Alignment; 45import android.text.StaticLayout; 46import android.text.TextPaint; 47import android.text.TextUtils; 48import android.text.format.DateFormat; 49import android.text.format.DateUtils; 50import android.text.format.Time; 51import android.util.Log; 52import android.view.ContextMenu; 53import android.view.ContextMenu.ContextMenuInfo; 54import android.view.GestureDetector; 55import android.view.Gravity; 56import android.view.KeyEvent; 57import android.view.LayoutInflater; 58import android.view.MenuItem; 59import android.view.MotionEvent; 60import android.view.ScaleGestureDetector; 61import android.view.View; 62import android.view.ViewConfiguration; 63import android.view.ViewGroup; 64import android.view.WindowManager; 65import android.view.accessibility.AccessibilityEvent; 66import android.view.animation.AccelerateDecelerateInterpolator; 67import android.view.animation.Animation; 68import android.view.animation.TranslateAnimation; 69import android.widget.ImageView; 70import android.widget.OverScroller; 71import android.widget.PopupWindow; 72import android.widget.TextView; 73import android.widget.ViewSwitcher; 74 75import java.util.ArrayList; 76import java.util.Arrays; 77import java.util.Calendar; 78import java.util.Formatter; 79import java.util.Locale; 80import java.util.regex.Matcher; 81import java.util.regex.Pattern; 82 83/** 84 * View for multi-day view. So far only 1 and 7 day have been tested. 85 */ 86public class DayView extends View implements View.OnCreateContextMenuListener, 87 ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener 88 { 89 private static String TAG = "DayView"; 90 private static boolean DEBUG = false; 91 private static final String HOUR_FORMAT_12H = "%A %I%p"; 92 private static final String HOUR_FORMAT_24H = "%A %H"; 93 private static final String PERIOD_SPACE = ". "; 94 95 private static float mScale = 0; // Used for supporting different screen densities 96 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event 97 private static final long ANIMATION_DURATION = 400; 98 99 private static final int MENU_AGENDA = 2; 100 private static final int MENU_DAY = 3; 101 private static final int MENU_EVENT_VIEW = 5; 102 private static final int MENU_EVENT_CREATE = 6; 103 private static final int MENU_EVENT_EDIT = 7; 104 private static final int MENU_EVENT_DELETE = 8; 105 106 private static int DEFAULT_CELL_HEIGHT = 64; 107 private static int MAX_CELL_HEIGHT = 150; 108 private static int MIN_Y_SPAN = 100; 109 110 private boolean mOnFlingCalled; 111 /** 112 * ID of the last event which was displayed with the toast popup. 113 * 114 * This is used to prevent popping up multiple quick views for the same event, especially 115 * during calendar syncs. This becomes valid when an event is selected, either by default 116 * on starting calendar or by scrolling to an event. It becomes invalid when the user 117 * explicitly scrolls to an empty time slot, changes views, or deletes the event. 118 */ 119 private long mLastPopupEventID; 120 121 protected Context mContext; 122 123 private static final String[] CALENDARS_PROJECTION = new String[] { 124 Calendars._ID, // 0 125 Calendars.ACCESS_LEVEL, // 1 126 Calendars.OWNER_ACCOUNT, // 2 127 }; 128 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; 129 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 130 private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; 131 132 private static final String[] ATTENDEES_PROJECTION = new String[] { 133 Attendees._ID, // 0 134 Attendees.ATTENDEE_RELATIONSHIP, // 1 135 }; 136 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1; 137 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d"; 138 139 private static final int FROM_NONE = 0; 140 private static final int FROM_ABOVE = 1; 141 private static final int FROM_BELOW = 2; 142 private static final int FROM_LEFT = 4; 143 private static final int FROM_RIGHT = 8; 144 145 private static final int ACCESS_LEVEL_NONE = 0; 146 private static final int ACCESS_LEVEL_DELETE = 1; 147 private static final int ACCESS_LEVEL_EDIT = 2; 148 149 private static int mHorizontalSnapBackThreshold = 128; 150 private static int HORIZONTAL_FLING_THRESHOLD = 75; 151 152 private ContinueScroll mContinueScroll = new ContinueScroll(); 153 154 // Make this visible within the package for more informative debugging 155 Time mBaseDate; 156 private Time mCurrentTime; 157 //Update the current time line every five minutes if the window is left open that long 158 private static final int UPDATE_CURRENT_TIME_DELAY = 300000; 159 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); 160 private int mTodayJulianDay; 161 162 private Typeface mBold = Typeface.DEFAULT_BOLD; 163 private int mFirstJulianDay; 164 private int mLastJulianDay; 165 166 private int mMonthLength; 167 private int mFirstVisibleDate; 168 private int mFirstVisibleDayOfWeek; 169 private int[] mEarliestStartHour; // indexed by the week day offset 170 private boolean[] mHasAllDayEvent; // indexed by the week day offset 171 private String mAllDayString; 172 private String mEventCountTemplate; 173 174 protected static StringBuilder mStringBuilder = new StringBuilder(50); 175 // TODO recreate formatter when locale changes 176 protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 177 178 private Runnable mTZUpdater = new Runnable() { 179 @Override 180 public void run() { 181 String tz = Utils.getTimeZone(mContext, this); 182 mBaseDate.timezone = tz; 183 mBaseDate.normalize(true); 184 mCurrentTime.switchTimezone(tz); 185 invalidate(); 186 } 187 }; 188 189 AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { 190 @Override 191 public void onAnimationStart(Animator animation) { 192 mScrolling = true; 193 } 194 195 @Override 196 public void onAnimationCancel(Animator animation) { 197 mScrolling = false; 198 } 199 200 @Override 201 public void onAnimationEnd(Animator animation) { 202 mScrolling = false; 203 resetSelectedHour(); 204 invalidate(); 205 } 206 }; 207 208 /** 209 * This variable helps to avoid unnecessarily reloading events by keeping 210 * track of the start millis parameter used for the most recent loading 211 * of events. If the next reload matches this, then the events are not 212 * reloaded. To force a reload, set this to zero (this is set to zero 213 * in the method clearCachedEvents()). 214 */ 215 private long mLastReloadMillis; 216 217 private ArrayList<Event> mEvents = new ArrayList<Event>(); 218 private ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); 219 private StaticLayout[] mLayouts = null; 220 private StaticLayout[] mAllDayLayouts = null; 221 private int mSelectionDay; // Julian day 222 private int mSelectionHour; 223 224 boolean mSelectionAllDay; 225 226 /** Width of a day or non-conflicting event */ 227 private int mCellWidth; 228 229 // Pre-allocate these objects and re-use them 230 private Rect mRect = new Rect(); 231 private Rect mDestRect = new Rect(); 232 private Rect mSelectionRect = new Rect(); 233 private Paint mPaint = new Paint(); 234 private Paint mEventTextPaint = new Paint(); 235 private Paint mSelectionPaint = new Paint(); 236 private float[] mLines; 237 238 private int mFirstDayOfWeek; // First day of the week 239 240 private PopupWindow mPopup; 241 private View mPopupView; 242 243 // The number of milliseconds to show the popup window 244 private static final int POPUP_DISMISS_DELAY = 3000; 245 private DismissPopup mDismissPopup = new DismissPopup(); 246 247 private boolean mRemeasure = true; 248 249 private final EventLoader mEventLoader; 250 protected final EventGeometry mEventGeometry; 251 252 private static float GRID_LINE_LEFT_MARGIN = 16; 253 private static final float GRID_LINE_INNER_WIDTH = 1; 254 private static final float GRID_LINE_WIDTH = 5; 255 256 private static final int DAY_GAP = 1; 257 private static final int HOUR_GAP = 1; 258 private static int SINGLE_ALLDAY_HEIGHT = 34; 259 private static int MAX_ALLDAY_HEIGHT = 105; 260 private static int ALLDAY_TOP_MARGIN = 3; 261 private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; 262 263 private static int HOURS_TOP_MARGIN = 2; 264 private static int HOURS_LEFT_MARGIN = 30; 265 private static int HOURS_RIGHT_MARGIN = 4; 266 private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 267 268 private static int CURRENT_TIME_LINE_HEIGHT = 2; 269 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1; 270 private static final int CURRENT_TIME_LINE_SIDE_BUFFER = 2; 271 272 /* package */ static final int MINUTES_PER_HOUR = 60; 273 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; 274 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; 275 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); 276 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 277 278 private static final int DAY_HEADER_ALPHA = 0x80000000; 279 private static final int DATE_HEADER_ALPHA = 0x26000000; 280 private static final int DATE_HEADER_TODAY_ALPHA = 0x99000000; 281 private static float DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; 282 private static float DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; 283 private static float DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; 284 private static float DAY_HEADER_LEFT_MARGIN = 5; 285 private static float DAY_HEADER_RIGHT_MARGIN = 7; 286 private static float DAY_HEADER_BOTTOM_MARGIN = 3; 287 private static float DAY_HEADER_FONT_SIZE = 14; 288 private static float DATE_HEADER_FONT_SIZE = 32; 289 private static float NORMAL_FONT_SIZE = 12; 290 private static float EVENT_TEXT_FONT_SIZE = 12; 291 private static float HOURS_FONT_SIZE = 12; 292 private static float AMPM_FONT_SIZE = 9; 293 private static int MIN_HOURS_WIDTH = 96; 294 private static int MIN_CELL_WIDTH_FOR_TEXT = 27; 295 private static final int MAX_EVENT_TEXT_LEN = 500; 296 private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels 297 private static int CALENDAR_COLOR_SQUARE_SIZE = 10; 298 private static int CALENDAR_COLOR_SQUARE_V_OFFSET = -1; 299 private static int CALENDAR_COLOR_SQUARE_H_OFFSET = -3; 300 private static int EVENT_RECT_TOP_MARGIN = -1; 301 private static int EVENT_RECT_BOTTOM_MARGIN = -1; 302 private static int EVENT_RECT_LEFT_MARGIN = -1; 303 private static int EVENT_RECT_RIGHT_MARGIN = -1; 304 private static int EVENT_TEXT_TOP_MARGIN = 2; 305 private static int EVENT_TEXT_BOTTOM_MARGIN = 3; 306 private static int EVENT_TEXT_LEFT_MARGIN = 11; 307 private static int EVENT_TEXT_RIGHT_MARGIN = 4; 308 private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 309 private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; 310 private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 311 private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; 312 313 private static int mPressedColor; 314 private static int mEventTextColor; 315 private static int mDeclinedEventTextColor; 316 317 private static int mWeek_saturdayColor; 318 private static int mWeek_sundayColor; 319 private static int mCalendarDateBannerTextColor; 320// private static int mCalendarAllDayBackground; 321 private static int mCalendarAmPmLabel; 322// private static int mCalendarDateBannerBackground; 323// private static int mCalendarDateSelected; 324// private static int mCalendarGridAreaBackground; 325 private static int mCalendarGridAreaSelected; 326 private static int mCalendarGridLineHorizontalColor; 327 private static int mCalendarGridLineVerticalColor; 328 private static int mCalendarGridLineInnerHorizontalColor; 329 private static int mCalendarGridLineInnerVerticalColor; 330// private static int mCalendarHourBackground; 331 private static int mCalendarHourLabelColor; 332// private static int mCalendarHourSelected; 333 334 private int mViewStartX; 335 private int mViewStartY; 336 private int mMaxViewStartY; 337 private int mViewHeight; 338 private int mViewWidth; 339 private int mGridAreaHeight = -1; 340 private static int mCellHeight = 0; // shared among all DayViews 341 private static int mMinCellHeight = 32; 342 private int mScrollStartY; 343 private int mPreviousDirection; 344 345 /** 346 * Vertical distance or span between the two touch points at the start of a 347 * scaling gesture 348 */ 349 private float mStartingSpanY = 0; 350 /** Height of 1 hour in pixels at the start of a scaling gesture */ 351 private int mCellHeightBeforeScaleGesture; 352 /** The hour at the center two touch points */ 353 private float mGestureCenterHour = 0; 354 /** 355 * Flag to decide whether to handle the up event. Cases where up events 356 * should be ignored are 1) right after a scale gesture and 2) finger was 357 * down before app launch 358 */ 359 private boolean mHandleActionUp = true; 360 361 private int mHoursTextHeight; 362 private int mAllDayHeight; 363 private static int DAY_HEADER_HEIGHT = 45; 364 /** 365 * Max of all day events in a given day in this view. 366 */ 367 private int mMaxAllDayEvents; 368 369 protected int mNumDays = 7; 370 private int mNumHours = 10; 371 372 /** Width of the time line (list of hours) to the left. */ 373 private int mHoursWidth; 374 private int mDateStrWidth; 375 /** Top of the scrollable region i.e. below date labels and all day events */ 376 private int mFirstCell; 377 /** First fully visibile hour */ 378 private int mFirstHour = -1; 379 /** Distance between the mFirstCell and the top of first fully visible hour. */ 380 private int mFirstHourOffset; 381 private String[] mHourStrs; 382 private String[] mDayStrs; 383 private String[] mDayStrs2Letter; 384 private boolean mIs24HourFormat; 385 386 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); 387 private boolean mComputeSelectedEvents; 388 private boolean mUpdateToast; 389 private Event mSelectedEvent; 390 private Event mPrevSelectedEvent; 391 private Rect mPrevBox = new Rect(); 392 protected final Resources mResources; 393 protected final Drawable mCurrentTimeLine; 394 protected final Drawable mTodayHeaderDrawable; 395 protected Drawable mAcceptedOrTentativeEventBoxDrawable; 396 protected Drawable mUnconfirmedOrDeclinedEventBoxDrawable; 397 private String mAmString; 398 private String mPmString; 399 private DeleteEventHelper mDeleteEventHelper; 400 private static int sCounter = 0; 401 402 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); 403 404 ScaleGestureDetector mScaleGestureDetector; 405 406 /** 407 * The initial state of the touch mode when we enter this view. 408 */ 409 private static final int TOUCH_MODE_INITIAL_STATE = 0; 410 411 /** 412 * Indicates we just received the touch event and we are waiting to see if 413 * it is a tap or a scroll gesture. 414 */ 415 private static final int TOUCH_MODE_DOWN = 1; 416 417 /** 418 * Indicates the touch gesture is a vertical scroll 419 */ 420 private static final int TOUCH_MODE_VSCROLL = 0x20; 421 422 /** 423 * Indicates the touch gesture is a horizontal scroll 424 */ 425 private static final int TOUCH_MODE_HSCROLL = 0x40; 426 427 private int mTouchMode = TOUCH_MODE_INITIAL_STATE; 428 429 /** 430 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. 431 */ 432 private static final int SELECTION_HIDDEN = 0; 433 private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet 434 private static final int SELECTION_SELECTED = 2; 435 private static final int SELECTION_LONGPRESS = 3; 436 437 private int mSelectionMode = SELECTION_HIDDEN; 438 439 private boolean mScrolling = false; 440 441 private CalendarController mController; 442 private ViewSwitcher mViewSwitcher; 443 private GestureDetector mGestureDetector; 444 private OverScroller mScroller; 445 446 public DayView(Context context, CalendarController controller, 447 ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { 448 super(context); 449 if (mScale == 0) { 450 mScale = getContext().getResources().getDisplayMetrics().density; 451 if (mScale != 1) { 452 SINGLE_ALLDAY_HEIGHT *= mScale; 453 ALLDAY_TOP_MARGIN *= mScale; 454 MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; 455 456 NORMAL_FONT_SIZE *= mScale; 457 EVENT_TEXT_FONT_SIZE *= mScale; 458 GRID_LINE_LEFT_MARGIN *= mScale; 459 HOURS_FONT_SIZE *= mScale; 460 HOURS_TOP_MARGIN *= mScale; 461 HOURS_LEFT_MARGIN *= mScale; 462 HOURS_RIGHT_MARGIN *= mScale; 463 HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 464 AMPM_FONT_SIZE *= mScale; 465 MIN_HOURS_WIDTH *= mScale; 466 MIN_CELL_WIDTH_FOR_TEXT *= mScale; 467 MIN_EVENT_HEIGHT *= mScale; 468 469 HORIZONTAL_FLING_THRESHOLD *= mScale; 470 471 CURRENT_TIME_LINE_HEIGHT *= mScale; 472 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale; 473 474 MIN_Y_SPAN *= mScale; 475 MAX_CELL_HEIGHT *= mScale; 476 DEFAULT_CELL_HEIGHT *= mScale; 477 DAY_HEADER_HEIGHT *= mScale; 478 DAY_HEADER_LEFT_MARGIN *= mScale; 479 DAY_HEADER_RIGHT_MARGIN *= mScale; 480 DAY_HEADER_BOTTOM_MARGIN *= mScale; 481 DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; 482 DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; 483 DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; 484 DAY_HEADER_FONT_SIZE *= mScale; 485 DATE_HEADER_FONT_SIZE *= mScale; 486 CALENDAR_COLOR_SQUARE_SIZE *= mScale; 487 EVENT_TEXT_TOP_MARGIN *= mScale; 488 EVENT_TEXT_BOTTOM_MARGIN *= mScale; 489 EVENT_TEXT_LEFT_MARGIN *= mScale; 490 EVENT_TEXT_RIGHT_MARGIN *= mScale; 491 EVENT_ALL_DAY_TEXT_TOP_MARGIN *= mScale; 492 EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN *= mScale; 493 EVENT_ALL_DAY_TEXT_LEFT_MARGIN *= mScale; 494 EVENT_ALL_DAY_TEXT_RIGHT_MARGIN *= mScale; 495 EVENT_RECT_TOP_MARGIN *= mScale; 496 EVENT_RECT_BOTTOM_MARGIN *= mScale; 497 EVENT_RECT_LEFT_MARGIN *= mScale; 498 EVENT_RECT_RIGHT_MARGIN *= mScale; 499 } 500 } 501 502 mResources = context.getResources(); 503 mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_week_holo_light); 504 mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); 505 mAcceptedOrTentativeEventBoxDrawable = mResources 506 .getDrawable(R.drawable.panel_month_event_holo_light); 507 mUnconfirmedOrDeclinedEventBoxDrawable = mResources 508 .getDrawable(R.drawable.week_event_bg_decline_holo_light); 509 mEventLoader = eventLoader; 510 mEventGeometry = new EventGeometry(); 511 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); 512 mEventGeometry.setHourGap(HOUR_GAP); 513 mContext = context; 514 mAllDayString = mContext.getString(R.string.edit_event_all_day_label); 515 mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); 516 mLastPopupEventID = INVALID_EVENT_ID; 517 mController = controller; 518 mViewSwitcher = viewSwitcher; 519 mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); 520 mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); 521 mNumDays = numDays; 522 if (mCellHeight == 0) { 523 mCellHeight = Utils.getSharedPreference(mContext, 524 GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); 525 } 526 mScroller = new OverScroller(context); 527 init(context); 528 } 529 530 private void init(Context context) { 531 setFocusable(true); 532 533 // Allow focus in touch mode so that we can do keyboard shortcuts 534 // even after we've entered touch mode. 535 setFocusableInTouchMode(true); 536 setClickable(true); 537 setOnCreateContextMenuListener(this); 538 539 mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); 540 541 mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); 542 long currentTime = System.currentTimeMillis(); 543 mCurrentTime.set(currentTime); 544 //The % makes it go off at the next increment of 5 minutes. 545 postDelayed(mUpdateCurrentTime, 546 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 547 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 548 549 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); 550 mWeek_sundayColor = mResources.getColor(R.color.week_sunday); 551 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); 552// mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background); 553 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); 554// mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background); 555// mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected); 556// mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background); 557 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); 558 mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color); 559 mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color); 560 mCalendarGridLineInnerHorizontalColor = mResources.getColor(R.color.calendar_grid_line_inner_horizontal_color); 561 mCalendarGridLineInnerVerticalColor = mResources.getColor(R.color.calendar_grid_line_inner_vertical_color); 562// mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background); 563 mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); 564// mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected); 565 mPressedColor = mResources.getColor(R.color.pressed); 566 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); 567 mDeclinedEventTextColor = mResources.getColor(R.color.calendar_declined_event_text_color); 568 569 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); 570 mEventTextPaint.setTextAlign(Paint.Align.LEFT); 571 mEventTextPaint.setAntiAlias(true); 572 573 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); 574 Paint p = mSelectionPaint; 575 p.setColor(gridLineColor); 576 p.setStyle(Style.FILL); 577 p.setAntiAlias(false); 578 579 p = mPaint; 580 p.setAntiAlias(true); 581 582 // Allocate space for 2 weeks worth of weekday names so that we can 583 // easily start the week display at any week day. 584 mDayStrs = new String[14]; 585 586 // Also create an array of 2-letter abbreviations. 587 mDayStrs2Letter = new String[14]; 588 589 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 590 int index = i - Calendar.SUNDAY; 591 // e.g. Tue for Tuesday 592 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM); 593 mDayStrs[index + 7] = mDayStrs[index]; 594 // e.g. Tu for Tuesday 595 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT); 596 597 // If we don't have 2-letter day strings, fall back to 1-letter. 598 if (mDayStrs2Letter[index].equals(mDayStrs[index])) { 599 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); 600 } 601 602 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; 603 } 604 605 // Figure out how much space we need for the 3-letter abbrev names 606 // in the worst case. 607 p.setTextSize(DATE_HEADER_FONT_SIZE); 608 p.setTypeface(mBold); 609 String[] dateStrs = {" 28", " 30"}; 610 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); 611 p.setTextSize(DAY_HEADER_FONT_SIZE); 612 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); 613 614 p.setTextSize(HOURS_FONT_SIZE); 615 p.setTypeface(null); 616 handleOnResume(); 617 618 mAmString = DateUtils.getAMPMString(Calendar.AM); 619 mPmString = DateUtils.getAMPMString(Calendar.PM); 620 String[] ampm = {mAmString, mPmString}; 621 p.setTextSize(AMPM_FONT_SIZE); 622 mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p) + HOURS_MARGIN; 623 mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); 624 625 LayoutInflater inflater; 626 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 627 mPopupView = inflater.inflate(R.layout.bubble_event, null); 628 mPopupView.setLayoutParams(new ViewGroup.LayoutParams( 629 ViewGroup.LayoutParams.MATCH_PARENT, 630 ViewGroup.LayoutParams.WRAP_CONTENT)); 631 mPopup = new PopupWindow(context); 632 mPopup.setContentView(mPopupView); 633 Resources.Theme dialogTheme = getResources().newTheme(); 634 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); 635 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { 636 android.R.attr.windowBackground }); 637 mPopup.setBackgroundDrawable(ta.getDrawable(0)); 638 ta.recycle(); 639 640 // Enable touching the popup window 641 mPopupView.setOnClickListener(this); 642 // Catch long clicks for creating a new event 643 setOnLongClickListener(this); 644 645 mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); 646 long millis = System.currentTimeMillis(); 647 mBaseDate.set(millis); 648 649 mEarliestStartHour = new int[mNumDays]; 650 mHasAllDayEvent = new boolean[mNumDays]; 651 652 // mLines is the array of points used with Canvas.drawLines() in 653 // drawGridBackground() and drawAllDayEvents(). Its size depends 654 // on the max number of lines that can ever be drawn by any single 655 // drawLines() call in either of those methods. 656 final int maxGridLines = (24 + 1) // max horizontal lines we might draw 657 + (mNumDays + 1); // max vertical lines we might draw 658 mLines = new float[maxGridLines * 4]; 659 } 660 661 /** 662 * This is called when the popup window is pressed. 663 */ 664 public void onClick(View v) { 665 if (v == mPopupView) { 666 // Pretend it was a trackball click because that will always 667 // jump to the "View event" screen. 668 switchViews(true /* trackball */); 669 } 670 } 671 672 public void handleOnResume() { 673 mIs24HourFormat = DateFormat.is24HourFormat(mContext); 674 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; 675 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 676 } 677 678 /** 679 * Returns the start of the selected time in milliseconds since the epoch. 680 * 681 * @return selected time in UTC milliseconds since the epoch. 682 */ 683 long getSelectedTimeInMillis() { 684 Time time = new Time(mBaseDate); 685 time.setJulianDay(mSelectionDay); 686 time.hour = mSelectionHour; 687 688 // We ignore the "isDst" field because we want normalize() to figure 689 // out the correct DST value and not adjust the selected time based 690 // on the current setting of DST. 691 return time.normalize(true /* ignore isDst */); 692 } 693 694 Time getSelectedTime() { 695 Time time = new Time(mBaseDate); 696 time.setJulianDay(mSelectionDay); 697 time.hour = mSelectionHour; 698 699 // We ignore the "isDst" field because we want normalize() to figure 700 // out the correct DST value and not adjust the selected time based 701 // on the current setting of DST. 702 time.normalize(true /* ignore isDst */); 703 return time; 704 } 705 706 /** 707 * Returns the start of the selected time in minutes since midnight, 708 * local time. The derived class must ensure that this is consistent 709 * with the return value from getSelectedTimeInMillis(). 710 */ 711 int getSelectedMinutesSinceMidnight() { 712 return mSelectionHour * MINUTES_PER_HOUR; 713 } 714 715 int getFirstVisibleHour() { 716 return mFirstHour; 717 } 718 719 void setFirstVisibleHour(int firstHour) { 720 mFirstHour = firstHour; 721 mFirstHourOffset = 0; 722 } 723 724 public void setSelected(Time time, boolean ignoreTime) { 725 mBaseDate.set(time); 726 mSelectionHour = mBaseDate.hour; 727 mSelectedEvent = null; 728 mPrevSelectedEvent = null; 729 long millis = mBaseDate.toMillis(false /* use isDst */); 730 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff); 731 mSelectedEvents.clear(); 732 mComputeSelectedEvents = true; 733 734 int gotoY = Integer.MIN_VALUE; 735 736 if (!ignoreTime && mGridAreaHeight != -1) { 737 int lastHour = 0; 738 739 if (mBaseDate.hour < mFirstHour) { 740 // Above visible region 741 gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); 742 } else { 743 lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) 744 + mFirstHour; 745 746 if (mBaseDate.hour >= lastHour) { 747 // Below visible region 748 749 // target hour + 1 (to give it room to see the event) - 750 // grid height (to get the y of the top of the visible 751 // region) 752 gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) 753 * (mCellHeight + HOUR_GAP) - mGridAreaHeight); 754 } 755 } 756 757 if (DEBUG) { 758 Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " 759 + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight 760 + " ymax " + mMaxViewStartY); 761 } 762 763 if (gotoY > mMaxViewStartY) { 764 gotoY = mMaxViewStartY; 765 } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { 766 gotoY = 0; 767 } 768 } 769 770 recalc(); 771 772 // Don't draw the selection box if we are going to the "current" time 773 long currMillis = System.currentTimeMillis(); 774 boolean recent = (currMillis - 10000) < millis && millis < currMillis; 775 mSelectionMode = (recent || ignoreTime) ? SELECTION_HIDDEN : SELECTION_SELECTED; 776 mRemeasure = true; 777 invalidate(); 778 779 if (gotoY != Integer.MIN_VALUE) { 780 TypeEvaluator evaluator = new TypeEvaluator() { 781 @Override 782 public Object evaluate(float fraction, Object startValue, Object endValue) { 783 int start = (Integer) startValue; 784 int end = (Integer) endValue; 785 final int newValue = (int) ((end - start) * fraction + start); 786 setViewStartY(newValue); 787 return new Integer(newValue); 788 } 789 }; 790 ValueAnimator scrollAnim = ObjectAnimator.ofObject(evaluator, new Integer(mViewStartY), 791 new Integer(gotoY)); 792// TODO The following line is supposed to replace the two statements above. 793// Need to investigate why it's not working. 794 795// ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); 796 scrollAnim.setDuration(200); 797 scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 798 scrollAnim.addListener(mAnimatorListener); 799 scrollAnim.start(); 800 } 801 } 802 803 public void setViewStartY(int viewStartY) { 804 if (viewStartY > mMaxViewStartY) { 805 viewStartY = mMaxViewStartY; 806 } 807 808 mViewStartY = viewStartY; 809 810 computeFirstHour(); 811 invalidate(); 812 } 813 814 public Time getSelectedDay() { 815 Time time = new Time(mBaseDate); 816 time.setJulianDay(mSelectionDay); 817 time.hour = mSelectionHour; 818 819 // We ignore the "isDst" field because we want normalize() to figure 820 // out the correct DST value and not adjust the selected time based 821 // on the current setting of DST. 822 time.normalize(true /* ignore isDst */); 823 return time; 824 } 825 826 public void updateTitle() { 827 Time start = new Time(mBaseDate); 828 start.normalize(true); 829 Time end = new Time(start); 830 end.monthDay += mNumDays - 1; 831 // Move it forward one minute so the formatter doesn't lose a day 832 end.minute += 1; 833 end.normalize(true); 834 835 long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 836 if (mNumDays != 1) { 837 // Don't show day of the month if for multi-day view 838 formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; 839 840 // Abbreviate the month if showing multiple months 841 if (start.month != end.month) { 842 formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; 843 } 844 } 845 846 mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, 847 formatFlags, null, null); 848 } 849 850 /** 851 * return a negative number if "time" is comes before the visible time 852 * range, a positive number if "time" is after the visible time range, and 0 853 * if it is in the visible time range. 854 */ 855 public int compareToVisibleTimeRange(Time time) { 856 857 int savedHour = mBaseDate.hour; 858 int savedMinute = mBaseDate.minute; 859 int savedSec = mBaseDate.second; 860 861 mBaseDate.hour = 0; 862 mBaseDate.minute = 0; 863 mBaseDate.second = 0; 864 865 if (DEBUG) { 866 Log.d(TAG, "Begin " + mBaseDate.toString()); 867 Log.d(TAG, "Diff " + time.toString()); 868 } 869 870 // Compare beginning of range 871 int diff = Time.compare(time, mBaseDate); 872 if (diff > 0) { 873 // Compare end of range 874 mBaseDate.monthDay += mNumDays; 875 mBaseDate.normalize(true); 876 diff = Time.compare(time, mBaseDate); 877 878 if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); 879 880 mBaseDate.monthDay -= mNumDays; 881 mBaseDate.normalize(true); 882 if (diff < 0) { 883 // in visible time 884 diff = 0; 885 } else if (diff == 0) { 886 // Midnight of following day 887 diff = 1; 888 } 889 } 890 891 if (DEBUG) Log.d(TAG, "Diff: " + diff); 892 893 mBaseDate.hour = savedHour; 894 mBaseDate.minute = savedMinute; 895 mBaseDate.second = savedSec; 896 return diff; 897 } 898 899 private void recalc() { 900 // Set the base date to the beginning of the week if we are displaying 901 // 7 days at a time. 902 if (mNumDays == 7) { 903 adjustToBeginningOfWeek(mBaseDate); 904 } 905 906 final long start = mBaseDate.toMillis(false /* use isDst */); 907 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); 908 mLastJulianDay = mFirstJulianDay + mNumDays - 1; 909 910 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); 911 mFirstVisibleDate = mBaseDate.monthDay; 912 mFirstVisibleDayOfWeek = mBaseDate.weekDay; 913 } 914 915 private void adjustToBeginningOfWeek(Time time) { 916 int dayOfWeek = time.weekDay; 917 int diff = dayOfWeek - mFirstDayOfWeek; 918 if (diff != 0) { 919 if (diff < 0) { 920 diff += 7; 921 } 922 time.monthDay -= diff; 923 time.normalize(true /* ignore isDst */); 924 } 925 } 926 927 @Override 928 protected void onSizeChanged(int width, int height, int oldw, int oldh) { 929 mViewWidth = width; 930 mViewHeight = height; 931 int gridAreaWidth = width - mHoursWidth; 932 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; 933 934 // This would be about 1 day worth in a 7 day view 935 mHorizontalSnapBackThreshold = width / 7; 936 937 Paint p = new Paint(); 938 p.setTextSize(HOURS_FONT_SIZE); 939 mHoursTextHeight = (int) Math.abs(p.ascent()); 940 remeasure(width, height); 941 } 942 943 /** 944 * Measures the space needed for various parts of the view after 945 * loading new events. This can change if there are all-day events. 946 */ 947 private void remeasure(int width, int height) { 948 949 // First, clear the array of earliest start times, and the array 950 // indicating presence of an all-day event. 951 for (int day = 0; day < mNumDays; day++) { 952 mEarliestStartHour[day] = 25; // some big number 953 mHasAllDayEvent[day] = false; 954 } 955 956 // Compute the layout relation between each event before measuring cell 957 // width, as the cell width should be adjusted along with the relation. 958 // 959 // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) 960 // We should mark them as "overwapped". Though they are not overwapped logically, but 961 // minimum cell height implicitly expands the cell height of A and it should look like 962 // (1:00pm - 1:15pm) after the cell height adjustment. 963 964 // Compute the space needed for the all-day events, if any. 965 // Make a pass over all the events, and keep track of the maximum 966 // number of all-day events in any one day. Also, keep track of 967 // the earliest event in each day. 968 int maxAllDayEvents = 0; 969 final ArrayList<Event> events = mEvents; 970 final int len = events.size(); 971 // Num of all-day-events on each day. 972 final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; 973 Arrays.fill(eventsCount, 0); 974 for (int ii = 0; ii < len; ii++) { 975 Event event = events.get(ii); 976 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { 977 continue; 978 } 979 if (event.drawAsAllDay()) { 980 final int firstDay = Math.max(event.startDay, mFirstJulianDay); 981 final int lastDay = Math.min(event.endDay, mLastJulianDay); 982 for (int day = firstDay; day <= lastDay; day++) { 983 final int count = ++eventsCount[day - mFirstJulianDay]; 984 if (maxAllDayEvents < count) { 985 maxAllDayEvents = count; 986 } 987 } 988 989 int daynum = event.startDay - mFirstJulianDay; 990 int durationDays = event.endDay - event.startDay + 1; 991 if (daynum < 0) { 992 durationDays += daynum; 993 daynum = 0; 994 } 995 if (daynum + durationDays > mNumDays) { 996 durationDays = mNumDays - daynum; 997 } 998 for (int day = daynum; durationDays > 0; day++, durationDays--) { 999 mHasAllDayEvent[day] = true; 1000 } 1001 } else { 1002 int daynum = event.startDay - mFirstJulianDay; 1003 int hour = event.startTime / 60; 1004 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 1005 mEarliestStartHour[daynum] = hour; 1006 } 1007 1008 // Also check the end hour in case the event spans more than 1009 // one day. 1010 daynum = event.endDay - mFirstJulianDay; 1011 hour = event.endTime / 60; 1012 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 1013 mEarliestStartHour[daynum] = hour; 1014 } 1015 } 1016 } 1017 mMaxAllDayEvents = maxAllDayEvents; 1018 1019 // Calculate mAllDayHeight 1020 mFirstCell = DAY_HEADER_HEIGHT; 1021 int allDayHeight = 0; 1022 if (maxAllDayEvents > 0) { 1023 // If there is at most one all-day event per day, then use less 1024 // space (but more than the space for a single event). 1025 if (maxAllDayEvents == 1) { 1026 allDayHeight = SINGLE_ALLDAY_HEIGHT; 1027 } else { 1028 // Allow the all-day area to grow in height depending on the 1029 // number of all-day events we need to show, up to a limit. 1030 allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 1031 if (allDayHeight > MAX_ALLDAY_HEIGHT) { 1032 allDayHeight = MAX_ALLDAY_HEIGHT; 1033 } 1034 } 1035 mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; 1036 } else { 1037 mSelectionAllDay = false; 1038 } 1039 mAllDayHeight = allDayHeight; 1040 1041 mGridAreaHeight = height - mFirstCell; 1042 1043 // The min is where 24 hours cover the entire visible area 1044 mMinCellHeight = (height - DAY_HEADER_HEIGHT) / 24; 1045 if (mCellHeight < mMinCellHeight) { 1046 mCellHeight = mMinCellHeight; 1047 } 1048 1049 mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); 1050 mEventGeometry.setHourHeight(mCellHeight); 1051 1052 final long minimumDurationMillis = (long) 1053 (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); 1054 Event.computePositions(events, minimumDurationMillis); 1055 1056 // Compute the top of our reachable view 1057 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 1058 if (DEBUG) { 1059 Log.e(TAG, "mViewStartY: " + mViewStartY); 1060 Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); 1061 } 1062 if (mViewStartY > mMaxViewStartY) { 1063 mViewStartY = mMaxViewStartY; 1064 computeFirstHour(); 1065 } 1066 1067 if (mFirstHour == -1) { 1068 initFirstHour(); 1069 mFirstHourOffset = 0; 1070 } 1071 1072 // When we change the base date, the number of all-day events may 1073 // change and that changes the cell height. When we switch dates, 1074 // we use the mFirstHourOffset from the previous view, but that may 1075 // be too large for the new view if the cell height is smaller. 1076 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { 1077 mFirstHourOffset = mCellHeight + HOUR_GAP - 1; 1078 } 1079 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; 1080 1081 final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); 1082 //When we get new events we don't want to dismiss the popup unless the event changes 1083 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { 1084 mPopup.dismiss(); 1085 } 1086 mPopup.setWidth(eventAreaWidth - 20); 1087 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 1088 } 1089 1090 /** 1091 * Initialize the state for another view. The given view is one that has 1092 * its own bitmap and will use an animation to replace the current view. 1093 * The current view and new view are either both Week views or both Day 1094 * views. They differ in their base date. 1095 * 1096 * @param view the view to initialize. 1097 */ 1098 private void initView(DayView view) { 1099 view.mSelectionHour = mSelectionHour; 1100 view.mSelectedEvents.clear(); 1101 view.mComputeSelectedEvents = true; 1102 view.mFirstHour = mFirstHour; 1103 view.mFirstHourOffset = mFirstHourOffset; 1104 view.remeasure(getWidth(), getHeight()); 1105 1106 view.mSelectedEvent = null; 1107 view.mPrevSelectedEvent = null; 1108 view.mFirstDayOfWeek = mFirstDayOfWeek; 1109 if (view.mEvents.size() > 0) { 1110 view.mSelectionAllDay = mSelectionAllDay; 1111 } else { 1112 view.mSelectionAllDay = false; 1113 } 1114 1115 // Redraw the screen so that the selection box will be redrawn. We may 1116 // have scrolled to a different part of the day in some other view 1117 // so the selection box in this view may no longer be visible. 1118 view.recalc(); 1119 } 1120 1121 /** 1122 * Switch to another view based on what was selected (an event or a free 1123 * slot) and how it was selected (by touch or by trackball). 1124 * 1125 * @param trackBallSelection true if the selection was made using the 1126 * trackball. 1127 */ 1128 private void switchViews(boolean trackBallSelection) { 1129 Event selectedEvent = mSelectedEvent; 1130 1131 mPopup.dismiss(); 1132 mLastPopupEventID = INVALID_EVENT_ID; 1133 if (mNumDays > 1) { 1134 // This is the Week view. 1135 // With touch, we always switch to Day/Agenda View 1136 // With track ball, if we selected a free slot, then create an event. 1137 // If we selected a specific event, switch to EventInfo view. 1138 if (trackBallSelection) { 1139 if (selectedEvent == null) { 1140 // Switch to the EditEvent view 1141 long startMillis = getSelectedTimeInMillis(); 1142 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1143 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 1144 startMillis, endMillis, 0, 0, -1); 1145 } else { 1146 // Switch to the EventInfo view 1147 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1148 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1149 getSelectedTimeInMillis()); 1150 } 1151 } else { 1152 // This was a touch selection. If the touch selected a single 1153 // unambiguous event, then view that event. Otherwise go to 1154 // Day/Agenda view. 1155 if (mSelectedEvents.size() == 1) { 1156 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1157 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1158 getSelectedTimeInMillis()); 1159 } 1160 } 1161 } else { 1162 // This is the Day view. 1163 // If we selected a free slot, then create an event. 1164 // If we selected an event, then go to the EventInfo view. 1165 if (selectedEvent == null) { 1166 // Switch to the EditEvent view 1167 long startMillis = getSelectedTimeInMillis(); 1168 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1169 1170 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, startMillis, 1171 endMillis, 0, 0, -1); 1172 } else { 1173 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1174 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1175 getSelectedTimeInMillis()); 1176 } 1177 } 1178 } 1179 1180 @Override 1181 public boolean onKeyUp(int keyCode, KeyEvent event) { 1182 mScrolling = false; 1183 long duration = event.getEventTime() - event.getDownTime(); 1184 1185 switch (keyCode) { 1186 case KeyEvent.KEYCODE_DPAD_CENTER: 1187 if (mSelectionMode == SELECTION_HIDDEN) { 1188 // Don't do anything unless the selection is visible. 1189 break; 1190 } 1191 1192 if (mSelectionMode == SELECTION_PRESSED) { 1193 // This was the first press when there was nothing selected. 1194 // Change the selection from the "pressed" state to the 1195 // the "selected" state. We treat short-press and 1196 // long-press the same here because nothing was selected. 1197 mSelectionMode = SELECTION_SELECTED; 1198 invalidate(); 1199 break; 1200 } 1201 1202 // Check the duration to determine if this was a short press 1203 if (duration < ViewConfiguration.getLongPressTimeout()) { 1204 switchViews(true /* trackball */); 1205 } else { 1206 mSelectionMode = SELECTION_LONGPRESS; 1207 invalidate(); 1208 performLongClick(); 1209 } 1210 break; 1211// case KeyEvent.KEYCODE_BACK: 1212// if (event.isTracking() && !event.isCanceled()) { 1213// mPopup.dismiss(); 1214// mContext.finish(); 1215// return true; 1216// } 1217// break; 1218 } 1219 return super.onKeyUp(keyCode, event); 1220 } 1221 1222 @Override 1223 public boolean onKeyDown(int keyCode, KeyEvent event) { 1224 if (mSelectionMode == SELECTION_HIDDEN) { 1225 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1226 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP 1227 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1228 // Display the selection box but don't move or select it 1229 // on this key press. 1230 mSelectionMode = SELECTION_SELECTED; 1231 invalidate(); 1232 return true; 1233 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 1234 // Display the selection box but don't select it 1235 // on this key press. 1236 mSelectionMode = SELECTION_PRESSED; 1237 invalidate(); 1238 return true; 1239 } 1240 } 1241 1242 mSelectionMode = SELECTION_SELECTED; 1243 mScrolling = false; 1244 boolean redraw; 1245 int selectionDay = mSelectionDay; 1246 1247 switch (keyCode) { 1248 case KeyEvent.KEYCODE_DEL: 1249 // Delete the selected event, if any 1250 Event selectedEvent = mSelectedEvent; 1251 if (selectedEvent == null) { 1252 return false; 1253 } 1254 mPopup.dismiss(); 1255 mLastPopupEventID = INVALID_EVENT_ID; 1256 1257 long begin = selectedEvent.startMillis; 1258 long end = selectedEvent.endMillis; 1259 long id = selectedEvent.id; 1260 mDeleteEventHelper.delete(begin, end, id, -1); 1261 return true; 1262 case KeyEvent.KEYCODE_ENTER: 1263 switchViews(true /* trackball or keyboard */); 1264 return true; 1265 case KeyEvent.KEYCODE_BACK: 1266 if (event.getRepeatCount() == 0) { 1267 event.startTracking(); 1268 return true; 1269 } 1270 return super.onKeyDown(keyCode, event); 1271 case KeyEvent.KEYCODE_DPAD_LEFT: 1272 if (mSelectedEvent != null) { 1273 mSelectedEvent = mSelectedEvent.nextLeft; 1274 } 1275 if (mSelectedEvent == null) { 1276 mLastPopupEventID = INVALID_EVENT_ID; 1277 selectionDay -= 1; 1278 } 1279 redraw = true; 1280 break; 1281 1282 case KeyEvent.KEYCODE_DPAD_RIGHT: 1283 if (mSelectedEvent != null) { 1284 mSelectedEvent = mSelectedEvent.nextRight; 1285 } 1286 if (mSelectedEvent == null) { 1287 mLastPopupEventID = INVALID_EVENT_ID; 1288 selectionDay += 1; 1289 } 1290 redraw = true; 1291 break; 1292 1293 case KeyEvent.KEYCODE_DPAD_UP: 1294 if (mSelectedEvent != null) { 1295 mSelectedEvent = mSelectedEvent.nextUp; 1296 } 1297 if (mSelectedEvent == null) { 1298 mLastPopupEventID = INVALID_EVENT_ID; 1299 if (!mSelectionAllDay) { 1300 mSelectionHour -= 1; 1301 adjustHourSelection(); 1302 mSelectedEvents.clear(); 1303 mComputeSelectedEvents = true; 1304 } 1305 } 1306 redraw = true; 1307 break; 1308 1309 case KeyEvent.KEYCODE_DPAD_DOWN: 1310 if (mSelectedEvent != null) { 1311 mSelectedEvent = mSelectedEvent.nextDown; 1312 } 1313 if (mSelectedEvent == null) { 1314 mLastPopupEventID = INVALID_EVENT_ID; 1315 if (mSelectionAllDay) { 1316 mSelectionAllDay = false; 1317 } else { 1318 mSelectionHour++; 1319 adjustHourSelection(); 1320 mSelectedEvents.clear(); 1321 mComputeSelectedEvents = true; 1322 } 1323 } 1324 redraw = true; 1325 break; 1326 1327 default: 1328 return super.onKeyDown(keyCode, event); 1329 } 1330 1331 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { 1332 DayView view = (DayView) mViewSwitcher.getNextView(); 1333 Time date = view.mBaseDate; 1334 date.set(mBaseDate); 1335 if (selectionDay < mFirstJulianDay) { 1336 date.monthDay -= mNumDays; 1337 } else { 1338 date.monthDay += mNumDays; 1339 } 1340 date.normalize(true /* ignore isDst */); 1341 view.mSelectionDay = selectionDay; 1342 1343 initView(view); 1344 1345 Time end = new Time(date); 1346 end.monthDay += mNumDays - 1; 1347 Log.d(TAG, "onKeyDown"); 1348 mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); 1349 return true; 1350 } 1351 if (mSelectionDay != selectionDay) { 1352 Time date = new Time(mBaseDate); 1353 date.setJulianDay(selectionDay); 1354 date.hour = mSelectionHour; 1355 mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); 1356 } 1357 mSelectionDay = selectionDay; 1358 mSelectedEvents.clear(); 1359 mComputeSelectedEvents = true; 1360 mUpdateToast = true; 1361 1362 if (redraw) { 1363 invalidate(); 1364 return true; 1365 } 1366 1367 return super.onKeyDown(keyCode, event); 1368 } 1369 1370 private class AccessibilityRunnable implements Runnable { 1371 int mEventType = AccessibilityEvent.TYPE_VIEW_SELECTED; 1372 @Override 1373 public void run() { 1374 sendAccessibilityEvent(mEventType); 1375 } 1376 } 1377 1378 private AccessibilityRunnable mDispatchAccessibilityEventRunnable = new AccessibilityRunnable(); 1379 1380 @Override 1381 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1382 if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SELECTED && 1383 event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) { 1384 return false; 1385 } 1386 StringBuilder b = new StringBuilder(getSelectedTime() 1387 .format(mIs24HourFormat ? HOUR_FORMAT_24H : HOUR_FORMAT_12H)); 1388 b.append(PERIOD_SPACE); 1389 int numEvents = mSelectedEvents.size(); 1390 if (mEventCountTemplate == null) { 1391 mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); 1392 } 1393 switch (event.getEventType()) { 1394 // When a new hour is selected we sent this event 1395 case AccessibilityEvent.TYPE_VIEW_SELECTED: 1396 int i = 1; 1397 for (Event calEvent : mSelectedEvents) { 1398 if (numEvents > 1) { 1399 mStringBuilder.setLength(0); 1400 b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); 1401 b.append(" "); 1402 } 1403 appendEventAccessibilityString(b, calEvent); 1404 } 1405 break; 1406 // When a different event is selected we send this event 1407 case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: 1408 if (mSelectedEvent != null) { 1409 if (numEvents > 1) { 1410 mStringBuilder.setLength(0); 1411 b.append(mFormatter.format(mEventCountTemplate, 1412 mSelectedEvents.indexOf(mSelectedEvent) + 1, numEvents)); 1413 b.append(" "); 1414 } 1415 appendEventAccessibilityString(b, mSelectedEvent); 1416 } 1417 break; 1418 default: 1419 break; 1420 } 1421 CharSequence msg = b.toString(); 1422 event.getText().add(msg); 1423 event.setAddedCount(msg.length()); 1424 return true; 1425 } 1426 1427 /** 1428 * @param b 1429 * @param calEvent 1430 */ 1431 private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { 1432 b.append(calEvent.getTitleAndLocation()); 1433 b.append(PERIOD_SPACE); 1434 String when; 1435 int flags = DateUtils.FORMAT_SHOW_DATE; 1436 if (calEvent.allDay) { 1437 flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; 1438 } else { 1439 flags |= DateUtils.FORMAT_SHOW_TIME; 1440 if (DateFormat.is24HourFormat(mContext)) { 1441 flags |= DateUtils.FORMAT_24HOUR; 1442 } 1443 } 1444 when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); 1445 b.append(when); 1446 b.append(PERIOD_SPACE); 1447 } 1448 1449 private class GotoBroadcaster implements Animation.AnimationListener { 1450 private final int mCounter; 1451 private final Time mStart; 1452 private final Time mEnd; 1453 1454 public GotoBroadcaster(Time start, Time end) { 1455 mCounter = ++sCounter; 1456 mStart = start; 1457 mEnd = end; 1458 } 1459 1460 @Override 1461 public void onAnimationEnd(Animation animation) { 1462 if (mCounter == sCounter) { 1463 mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, 1464 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 1465 } 1466 } 1467 1468 @Override 1469 public void onAnimationRepeat(Animation animation) { 1470 } 1471 1472 @Override 1473 public void onAnimationStart(Animation animation) { 1474 } 1475 } 1476 1477 private View switchViews(boolean forward, float xOffSet, float width) { 1478 if (DEBUG) Log.d(TAG, "switchViews(" + forward + ")..."); 1479 float progress = Math.abs(xOffSet) / width; 1480 if (progress > 1.0f) { 1481 progress = 1.0f; 1482 } 1483 1484 float inFromXValue, inToXValue; 1485 float outFromXValue, outToXValue; 1486 if (forward) { 1487 inFromXValue = 1.0f - progress; 1488 inToXValue = 0.0f; 1489 outFromXValue = -progress; 1490 outToXValue = -1.0f; 1491 } else { 1492 inFromXValue = progress - 1.0f; 1493 inToXValue = 0.0f; 1494 outFromXValue = progress; 1495 outToXValue = 1.0f; 1496 } 1497 1498 final Time start = new Time(mBaseDate.timezone); 1499 start.set(mController.getTime()); 1500 if (forward) { 1501 start.monthDay += mNumDays; 1502 } else { 1503 start.monthDay -= mNumDays; 1504 } 1505 mController.setTime(start.normalize(true)); 1506 1507 Time newSelected = start; 1508 1509 if (mNumDays == 7) { 1510 newSelected = new Time(start); 1511 adjustToBeginningOfWeek(start); 1512 } 1513 1514 final Time end = new Time(start); 1515 end.monthDay += mNumDays - 1; 1516 1517 // We have to allocate these animation objects each time we switch views 1518 // because that is the only way to set the animation parameters. 1519 TranslateAnimation inAnimation = new TranslateAnimation( 1520 Animation.RELATIVE_TO_SELF, inFromXValue, 1521 Animation.RELATIVE_TO_SELF, inToXValue, 1522 Animation.ABSOLUTE, 0.0f, 1523 Animation.ABSOLUTE, 0.0f); 1524 1525 TranslateAnimation outAnimation = new TranslateAnimation( 1526 Animation.RELATIVE_TO_SELF, outFromXValue, 1527 Animation.RELATIVE_TO_SELF, outToXValue, 1528 Animation.ABSOLUTE, 0.0f, 1529 Animation.ABSOLUTE, 0.0f); 1530 1531 // Reduce the animation duration based on how far we have already swiped. 1532 long duration = (long) (ANIMATION_DURATION * (1.0f - progress)); 1533 inAnimation.setDuration(duration); 1534 outAnimation.setDuration(duration); 1535 outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); 1536 mViewSwitcher.setInAnimation(inAnimation); 1537 mViewSwitcher.setOutAnimation(outAnimation); 1538 1539 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1540 view.cleanup(); 1541 mViewSwitcher.showNext(); 1542 view = (DayView) mViewSwitcher.getCurrentView(); 1543 view.setSelected(newSelected, true); 1544 view.requestFocus(); 1545 view.reloadEvents(); 1546 view.updateTitle(); 1547 1548 return view; 1549 } 1550 1551 // This is called after scrolling stops to move the selected hour 1552 // to the visible part of the screen. 1553 private void resetSelectedHour() { 1554 if (mSelectionHour < mFirstHour + 1) { 1555 mSelectionHour = mFirstHour + 1; 1556 mSelectedEvent = null; 1557 mSelectedEvents.clear(); 1558 mComputeSelectedEvents = true; 1559 } else if (mSelectionHour > mFirstHour + mNumHours - 3) { 1560 mSelectionHour = mFirstHour + mNumHours - 3; 1561 mSelectedEvent = null; 1562 mSelectedEvents.clear(); 1563 mComputeSelectedEvents = true; 1564 } 1565 } 1566 1567 private void initFirstHour() { 1568 mFirstHour = mSelectionHour - mNumHours / 5; 1569 if (mFirstHour < 0) { 1570 mFirstHour = 0; 1571 } else if (mFirstHour + mNumHours > 24) { 1572 mFirstHour = 24 - mNumHours; 1573 } 1574 } 1575 1576 /** 1577 * Recomputes the first full hour that is visible on screen after the 1578 * screen is scrolled. 1579 */ 1580 private void computeFirstHour() { 1581 // Compute the first full hour that is visible on screen 1582 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); 1583 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; 1584 } 1585 1586 private void adjustHourSelection() { 1587 if (mSelectionHour < 0) { 1588 mSelectionHour = 0; 1589 if (mMaxAllDayEvents > 0) { 1590 mPrevSelectedEvent = null; 1591 mSelectionAllDay = true; 1592 } 1593 } 1594 1595 if (mSelectionHour > 23) { 1596 mSelectionHour = 23; 1597 } 1598 1599 // If the selected hour is at least 2 time slots from the top and 1600 // bottom of the screen, then don't scroll the view. 1601 if (mSelectionHour < mFirstHour + 1) { 1602 // If there are all-days events for the selected day but there 1603 // are no more normal events earlier in the day, then jump to 1604 // the all-day event area. 1605 // Exception 1: allow the user to scroll to 8am with the trackball 1606 // before jumping to the all-day event area. 1607 // Exception 2: if 12am is on screen, then allow the user to select 1608 // 12am before going up to the all-day event area. 1609 int daynum = mSelectionDay - mFirstJulianDay; 1610 if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1611 && mFirstHour > 0 && mFirstHour < 8) { 1612 mPrevSelectedEvent = null; 1613 mSelectionAllDay = true; 1614 mSelectionHour = mFirstHour + 1; 1615 return; 1616 } 1617 1618 if (mFirstHour > 0) { 1619 mFirstHour -= 1; 1620 mViewStartY -= (mCellHeight + HOUR_GAP); 1621 if (mViewStartY < 0) { 1622 mViewStartY = 0; 1623 } 1624 return; 1625 } 1626 } 1627 1628 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1629 if (mFirstHour < 24 - mNumHours) { 1630 mFirstHour += 1; 1631 mViewStartY += (mCellHeight + HOUR_GAP); 1632 if (mViewStartY > mMaxViewStartY) { 1633 mViewStartY = mMaxViewStartY; 1634 } 1635 return; 1636 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1637 mViewStartY = mMaxViewStartY; 1638 } 1639 } 1640 } 1641 1642 void clearCachedEvents() { 1643 mLastReloadMillis = 0; 1644 } 1645 1646 private Runnable mCancelCallback = new Runnable() { 1647 public void run() { 1648 clearCachedEvents(); 1649 } 1650 }; 1651 1652 /* package */ void reloadEvents() { 1653 // Protect against this being called before this view has been 1654 // initialized. 1655// if (mContext == null) { 1656// return; 1657// } 1658 1659 // Make sure our time zones are up to date 1660 mTZUpdater.run(); 1661 1662 mSelectedEvent = null; 1663 mPrevSelectedEvent = null; 1664 mSelectedEvents.clear(); 1665 1666 // The start date is the beginning of the week at 12am 1667 Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); 1668 weekStart.set(mBaseDate); 1669 weekStart.hour = 0; 1670 weekStart.minute = 0; 1671 weekStart.second = 0; 1672 long millis = weekStart.normalize(true /* ignore isDst */); 1673 1674 // Avoid reloading events unnecessarily. 1675 if (millis == mLastReloadMillis) { 1676 return; 1677 } 1678 mLastReloadMillis = millis; 1679 1680 // load events in the background 1681// mContext.startProgressSpinner(); 1682 final ArrayList<Event> events = new ArrayList<Event>(); 1683 mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() { 1684 public void run() { 1685 mEvents = events; 1686 if (mAllDayEvents == null) { 1687 mAllDayEvents = new ArrayList<Event>(); 1688 } else { 1689 mAllDayEvents.clear(); 1690 } 1691 1692 // Create a shorter array for all day events 1693 for (Event e : events) { 1694 if (e.drawAsAllDay()) { 1695 mAllDayEvents.add(e); 1696 } 1697 } 1698 1699 // New events, new layouts 1700 if (mLayouts == null || mLayouts.length < events.size()) { 1701 mLayouts = new StaticLayout[events.size()]; 1702 } else { 1703 Arrays.fill(mLayouts, null); 1704 } 1705 1706 if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { 1707 mAllDayLayouts = new StaticLayout[events.size()]; 1708 } else { 1709 Arrays.fill(mAllDayLayouts, null); 1710 } 1711 1712 mRemeasure = true; 1713 mComputeSelectedEvents = true; 1714 recalc(); 1715// mContext.stopProgressSpinner(); 1716 invalidate(); 1717 } 1718 }, mCancelCallback); 1719 } 1720 1721 @Override 1722 protected void onDraw(Canvas canvas) { 1723 if (mRemeasure) { 1724 remeasure(getWidth(), getHeight()); 1725 mRemeasure = false; 1726 } 1727 canvas.save(); 1728 1729 float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAllDayHeight; 1730 // offset canvas by the current drag and header position 1731 canvas.translate(-mViewStartX, yTranslate); 1732 // clip to everything below the allDay area 1733 Rect dest = mDestRect; 1734 dest.top = (int) (mFirstCell - yTranslate); 1735 dest.bottom = (int) (mViewHeight - yTranslate); 1736 dest.left = 0; 1737 dest.right = mViewWidth; 1738 canvas.save(); 1739 canvas.clipRect(dest); 1740 // Draw the movable part of the view 1741 doDraw(canvas); 1742 // restore to having no clip 1743 canvas.restore(); 1744 1745 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 1746 float xTranslate; 1747 if (mViewStartX > 0) { 1748 xTranslate = mViewWidth; 1749 } else { 1750 xTranslate = -mViewWidth; 1751 } 1752 // Move the canvas around to prep it for the next view 1753 // specifically, shift it by a screen and undo the 1754 // yTranslation which will be redone in the nextView's onDraw(). 1755 canvas.translate(xTranslate, -yTranslate); 1756 DayView nextView = (DayView) mViewSwitcher.getNextView(); 1757 1758 // Prevent infinite recursive calls to onDraw(). 1759 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 1760 1761 nextView.onDraw(canvas); 1762 // Move it back for this view 1763 canvas.translate(-xTranslate, 0); 1764 } else { 1765 // If we drew another view we already translated it back 1766 // If we didn't draw another view we should be at the edge of the 1767 // screen 1768 canvas.translate(mViewStartX, -yTranslate); 1769 } 1770 1771 // Draw the fixed areas (that don't scroll) directly to the canvas. 1772 drawAfterScroll(canvas); 1773 if (mComputeSelectedEvents && mUpdateToast) { 1774 updateEventDetails(); 1775 mUpdateToast = false; 1776 } 1777 mComputeSelectedEvents = false; 1778 canvas.restore(); 1779 } 1780 1781 private void drawAfterScroll(Canvas canvas) { 1782 Paint p = mPaint; 1783 Rect r = mRect; 1784 1785 if (mMaxAllDayEvents != 0) { 1786 drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); 1787 drawUpperLeftCorner(r, canvas, p); 1788 } 1789 1790 drawScrollLine(r, canvas, p); 1791 1792 drawDayHeaderLoop(r, canvas, p); 1793 1794 // Draw the AM and PM indicators if we're in 12 hour mode 1795 if (!mIs24HourFormat) { 1796 drawAmPm(canvas, p); 1797 } 1798 } 1799 1800 // This isn't really the upper-left corner. It's the square area just 1801 // below the upper-left corner, above the hours and to the left of the 1802 // all-day area. 1803 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 1804 setupHourTextPaint(p); 1805 canvas.drawText(mAllDayString, HOURS_LEFT_MARGIN, DAY_HEADER_HEIGHT + HOURS_TOP_MARGIN 1806 + HOUR_GAP + mHoursTextHeight, p); 1807 } 1808 1809 private void drawScrollLine(Rect r, Canvas canvas, Paint p) { 1810 final int right = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays; 1811 final int y = mFirstCell - 1; 1812 1813 p.setAntiAlias(false); 1814 p.setStyle(Style.FILL); 1815 1816 p.setColor(mCalendarGridLineHorizontalColor); 1817 p.setStrokeWidth(GRID_LINE_WIDTH); 1818 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 1819 1820 p.setColor(mCalendarGridLineInnerHorizontalColor); 1821 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 1822 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 1823 p.setAntiAlias(true); 1824 } 1825 1826 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 1827 // Draw the horizontal day background banner 1828 // p.setColor(mCalendarDateBannerBackground); 1829 // r.top = 0; 1830 // r.bottom = DAY_HEADER_HEIGHT; 1831 // r.left = 0; 1832 // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 1833 // canvas.drawRect(r, p); 1834 // 1835 // Fill the extra space on the right side with the default background 1836 // r.left = r.right; 1837 // r.right = mViewWidth; 1838 // p.setColor(mCalendarGridAreaBackground); 1839 // canvas.drawRect(r, p); 1840 1841 int todayNum = mTodayJulianDay - mFirstJulianDay; 1842 if (mNumDays > 1) { 1843 r.top = 0; 1844 r.bottom = DAY_HEADER_HEIGHT; 1845 1846 // Highlight today 1847 if (mFirstJulianDay <= mTodayJulianDay 1848 && mTodayJulianDay < (mFirstJulianDay + mNumDays)) { 1849 r.left = mHoursWidth + todayNum * (mCellWidth + DAY_GAP) - DAY_GAP; 1850 r.right = r.left + mCellWidth; 1851 mTodayHeaderDrawable.setBounds(r); 1852 mTodayHeaderDrawable.draw(canvas); 1853 } 1854 1855 // Draw a highlight on the selected day (if any), but only if we are 1856 // displaying more than one day. 1857 // 1858 // int selectedDayNum = mSelectionDay - mFirstJulianDay; 1859 // if (mSelectionMode != SELECTION_HIDDEN && selectedDayNum >= 0 1860 // && selectedDayNum < mNumDays) { 1861 // p.setColor(mCalendarDateSelected); 1862 // r.left = mHoursWidth + selectedDayNum * (mCellWidth + DAY_GAP); 1863 // r.right = r.left + mCellWidth; 1864 // canvas.drawRect(r, p); 1865 // } 1866 } 1867 1868 p.setTypeface(mBold); 1869 p.setTextAlign(Paint.Align.RIGHT); 1870 int deltaX = mCellWidth + DAY_GAP; 1871 int cell = mFirstJulianDay; 1872 float x = mHoursWidth; 1873 if (mNumDays == 1) { 1874 x = HOURS_LEFT_MARGIN; 1875 } 1876 1877 String[] dayNames; 1878 if (mDateStrWidth < mCellWidth) { 1879 dayNames = mDayStrs; 1880 } else { 1881 dayNames = mDayStrs2Letter; 1882 } 1883 1884 p.setAntiAlias(true); 1885 for (int day = 0; day < mNumDays; day++, cell++) { 1886 int dayOfWeek = day + mFirstVisibleDayOfWeek; 1887 if (dayOfWeek >= 14) { 1888 dayOfWeek -= 14; 1889 } 1890 1891 int color = mCalendarDateBannerTextColor; 1892 if (mNumDays == 1) { 1893 if (dayOfWeek == Time.SATURDAY) { 1894 color = mWeek_saturdayColor; 1895 } else if (dayOfWeek == Time.SUNDAY) { 1896 color = mWeek_sundayColor; 1897 } 1898 } else { 1899 final int column = day % 7; 1900 if (Utils.isSaturday(column, mFirstDayOfWeek)) { 1901 color = mWeek_saturdayColor; 1902 } else if (Utils.isSunday(column, mFirstDayOfWeek)) { 1903 color = mWeek_sundayColor; 1904 } 1905 } 1906 1907 color &= 0x00FFFFFF; 1908 if (todayNum == day) { 1909 color |= DATE_HEADER_TODAY_ALPHA; 1910 } else { 1911 color |= DATE_HEADER_ALPHA; 1912 } 1913 1914 p.setColor(color); 1915 drawDayHeader(dayNames[dayOfWeek], day, cell, x, canvas, p); 1916 x += deltaX; 1917 } 1918 p.setTypeface(null); 1919 } 1920 1921 private void drawAmPm(Canvas canvas, Paint p) { 1922 p.setColor(mCalendarAmPmLabel); 1923 p.setTextSize(AMPM_FONT_SIZE); 1924 p.setTypeface(mBold); 1925 p.setAntiAlias(true); 1926 mPaint.setTextAlign(Paint.Align.LEFT); 1927 String text = mAmString; 1928 if (mFirstHour >= 12) { 1929 text = mPmString; 1930 } 1931 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 1932 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 1933 1934 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 1935 // Also draw the "PM" 1936 text = mPmString; 1937 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 1938 + 2 * mHoursTextHeight + HOUR_GAP; 1939 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 1940 } 1941 } 1942 1943 private void drawCurrentTimeLine(Rect r, final int left, final int top, Canvas canvas, 1944 Paint p) { 1945 r.left = left - CURRENT_TIME_LINE_SIDE_BUFFER; 1946 r.right = left + mCellWidth + DAY_GAP + CURRENT_TIME_LINE_SIDE_BUFFER; 1947 1948 r.top = top - mCurrentTimeLine.getIntrinsicHeight() / 2; 1949 r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); 1950 1951 mCurrentTimeLine.setBounds(r); 1952 mCurrentTimeLine.draw(canvas); 1953 } 1954 1955 private void doDraw(Canvas canvas) { 1956 Paint p = mPaint; 1957 Rect r = mRect; 1958 1959 drawGridBackground(r, canvas, p); 1960 drawHours(r, canvas, p); 1961 1962 // Draw each day 1963 int x = mHoursWidth; 1964 int deltaX = mCellWidth + DAY_GAP; 1965 int cell = mFirstJulianDay; 1966 for (int day = 0; day < mNumDays; day++, cell++) { 1967 // TODO Wow, this needs cleanup. drawEvents loop through all the 1968 // events on every call. 1969 drawEvents(cell, x, HOUR_GAP, canvas, p); 1970 // If this is today 1971 if (cell == mTodayJulianDay) { 1972 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 1973 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 1974 1975 // And the current time shows up somewhere on the screen 1976 if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 1977 drawCurrentTimeLine(r, x, lineY, canvas, p); 1978 } 1979 } 1980 x += deltaX; 1981 } 1982 } 1983 1984 private void drawHours(Rect r, Canvas canvas, Paint p) { 1985 // Comment out as the background will be a drawable 1986 1987 // Draw the background for the hour labels 1988 // p.setColor(mCalendarHourBackground); 1989 // r.top = 0; 1990 // r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP; 1991 // r.left = 0; 1992 // r.right = mHoursWidth; 1993 // canvas.drawRect(r, p); 1994 1995 // Fill the bottom left corner with the default grid background 1996 // r.top = r.bottom; 1997 // r.bottom = mBitmapHeight; 1998 // p.setColor(mCalendarGridAreaBackground); 1999 // canvas.drawRect(r, p); 2000 2001 // Draw a highlight on the selected hour (if needed) 2002 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) { 2003 // p.setColor(mCalendarHourSelected); 2004 int daynum = mSelectionDay - mFirstJulianDay; 2005 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2006 r.bottom = r.top + mCellHeight + HOUR_GAP; 2007 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + DAY_GAP; 2008 r.right = r.left + mCellWidth + DAY_GAP; 2009 2010 // Draw a border around the highlighted grid hour. 2011 // drawEmptyRect(canvas, r, mSelectionPaint.getColor()); 2012 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 2013 2014 // Also draw the highlight on the grid 2015 p.setColor(mCalendarGridAreaSelected); 2016 r.top += HOUR_GAP; 2017 r.right -= DAY_GAP; 2018 canvas.drawRect(r, p); 2019 } 2020 2021 setupHourTextPaint(p); 2022 2023 int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; 2024 2025 for (int i = 0; i < 24; i++) { 2026 String time = mHourStrs[i]; 2027 canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); 2028 y += mCellHeight + HOUR_GAP; 2029 } 2030 } 2031 2032 private void setupHourTextPaint(Paint p) { 2033 p.setColor(mCalendarHourLabelColor); 2034 p.setTextSize(HOURS_FONT_SIZE); 2035 p.setTypeface(Typeface.DEFAULT); 2036 p.setTextAlign(Paint.Align.LEFT); 2037 p.setAntiAlias(true); 2038 } 2039 2040 private void drawDayHeader(String dayStr, int day, int cell, float x, Canvas canvas, Paint p) { 2041 int dateNum = mFirstVisibleDate + day; 2042 if (dateNum > mMonthLength) { 2043 dateNum -= mMonthLength; 2044 } 2045 2046 // Draw day of the month 2047 String dateNumStr = String.valueOf(dateNum); 2048 if (mNumDays > 1) { 2049 float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; 2050 2051 // Draw day of the month 2052 x += mCellWidth - DAY_HEADER_RIGHT_MARGIN; 2053 p.setTextSize(DATE_HEADER_FONT_SIZE); 2054 p.setTypeface(mBold); 2055 canvas.drawText(dateNumStr, x, y, p); 2056 2057 // Draw day of the week 2058 x -= p.measureText(dateNumStr) + DAY_HEADER_LEFT_MARGIN; 2059 p.setColor((p.getColor() & 0x00FFFFFF) | DAY_HEADER_ALPHA); 2060 p.setTextSize(DAY_HEADER_FONT_SIZE); 2061 p.setTypeface(Typeface.DEFAULT); 2062 canvas.drawText(dayStr, x, y, p); 2063 } else { 2064 float y = DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; 2065 p.setTextAlign(Paint.Align.LEFT); 2066 2067 int dateColor = p.getColor(); 2068 2069 // Draw day of the week 2070 x += DAY_HEADER_ONE_DAY_LEFT_MARGIN; 2071 p.setColor((dateColor & 0x00FFFFFF) | DAY_HEADER_ALPHA); 2072 p.setTextSize(DAY_HEADER_FONT_SIZE); 2073 p.setTypeface(Typeface.DEFAULT); 2074 canvas.drawText(dayStr, x, y, p); 2075 2076 // Draw day of the month 2077 x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; 2078 p.setColor(dateColor); 2079 p.setTextSize(DATE_HEADER_FONT_SIZE); 2080 p.setTypeface(mBold); 2081 canvas.drawText(dateNumStr, x, y, p); 2082 } 2083 } 2084 2085 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 2086 Paint.Style savedStyle = p.getStyle(); 2087 2088 // Draw the outer horizontal grid lines 2089 p.setColor(mCalendarGridLineHorizontalColor); 2090 p.setStyle(Style.FILL); 2091 2092 p.setAntiAlias(false); 2093 final float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays; 2094 float y = 0; 2095 final float deltaY = mCellHeight + HOUR_GAP; 2096 p.setStrokeWidth(GRID_LINE_WIDTH); 2097 int linesIndex = 0; 2098 for (int hour = 0; hour <= 24; hour++) { 2099 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2100 mLines[linesIndex++] = y; 2101 mLines[linesIndex++] = stopX; 2102 mLines[linesIndex++] = y; 2103 y += deltaY; 2104 } 2105 if (mCalendarGridLineVerticalColor != mCalendarGridLineHorizontalColor) { 2106 canvas.drawLines(mLines, 0, linesIndex, p); 2107 linesIndex = 0; 2108 p.setColor(mCalendarGridLineVerticalColor); 2109 } 2110 2111 // Draw the outer vertical grid lines 2112 final float startY = 0; 2113 final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 2114 final float deltaX = mCellWidth + DAY_GAP; 2115 float x = mHoursWidth; 2116 for (int day = 0; day < mNumDays; day++) { 2117 x += deltaX; 2118 mLines[linesIndex++] = x; 2119 mLines[linesIndex++] = startY; 2120 mLines[linesIndex++] = x; 2121 mLines[linesIndex++] = stopY; 2122 } 2123 canvas.drawLines(mLines, 0, linesIndex, p); 2124 2125 // Draw the inner horizontal grid lines 2126 p.setColor(mCalendarGridLineInnerHorizontalColor); 2127 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2128 y = 0; 2129 linesIndex = 0; 2130 for (int hour = 0; hour <= 24; hour++) { 2131 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2132 mLines[linesIndex++] = y; 2133 mLines[linesIndex++] = stopX; 2134 mLines[linesIndex++] = y; 2135 y += deltaY; 2136 } 2137 if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { 2138 canvas.drawLines(mLines, 0, linesIndex, p); 2139 linesIndex = 0; 2140 p.setColor(mCalendarGridLineInnerVerticalColor); 2141 } 2142 2143 // Draw the inner vertical grid lines 2144 x = mHoursWidth; 2145 for (int day = 0; day < mNumDays; day++) { 2146 x += deltaX; 2147 mLines[linesIndex++] = x; 2148 mLines[linesIndex++] = startY; 2149 mLines[linesIndex++] = x; 2150 mLines[linesIndex++] = stopY; 2151 } 2152 canvas.drawLines(mLines, 0, linesIndex, p); 2153 2154 // Restore the saved style. 2155 p.setStyle(savedStyle); 2156 p.setAntiAlias(true); 2157 } 2158 2159 Event getSelectedEvent() { 2160 if (mSelectedEvent == null) { 2161 // There is no event at the selected hour, so create a new event. 2162 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2163 getSelectedMinutesSinceMidnight()); 2164 } 2165 return mSelectedEvent; 2166 } 2167 2168 boolean isEventSelected() { 2169 return (mSelectedEvent != null); 2170 } 2171 2172 Event getNewEvent() { 2173 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2174 getSelectedMinutesSinceMidnight()); 2175 } 2176 2177 static Event getNewEvent(int julianDay, long utcMillis, 2178 int minutesSinceMidnight) { 2179 Event event = Event.newInstance(); 2180 event.startDay = julianDay; 2181 event.endDay = julianDay; 2182 event.startMillis = utcMillis; 2183 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 2184 event.startTime = minutesSinceMidnight; 2185 event.endTime = event.startTime + MINUTES_PER_HOUR; 2186 return event; 2187 } 2188 2189 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 2190 float maxWidthF = 0.0f; 2191 2192 int len = strings.length; 2193 for (int i = 0; i < len; i++) { 2194 float width = p.measureText(strings[i]); 2195 maxWidthF = Math.max(width, maxWidthF); 2196 } 2197 int maxWidth = (int) (maxWidthF + 0.5); 2198 if (maxWidth < currentMax) { 2199 maxWidth = currentMax; 2200 } 2201 return maxWidth; 2202 } 2203 2204 private void saveSelectionPosition(float left, float top, float right, float bottom) { 2205 mPrevBox.left = (int) left; 2206 mPrevBox.right = (int) right; 2207 mPrevBox.top = (int) top; 2208 mPrevBox.bottom = (int) bottom; 2209 } 2210 2211 private Rect getCurrentSelectionPosition() { 2212 Rect box = new Rect(); 2213 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2214 box.bottom = box.top + mCellHeight + HOUR_GAP; 2215 int daynum = mSelectionDay - mFirstJulianDay; 2216 box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP); 2217 box.right = box.left + mCellWidth + DAY_GAP; 2218 return box; 2219 } 2220 2221 private void setupTextRect(Rect r) { 2222 if (r.bottom <= r.top || r.right <= r.left) { 2223 r.bottom = r.top; 2224 r.right = r.left; 2225 return; 2226 } 2227 2228 if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { 2229 r.top += EVENT_TEXT_TOP_MARGIN; 2230 r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; 2231 } 2232 if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { 2233 r.left += EVENT_TEXT_LEFT_MARGIN; 2234 r.right -= EVENT_TEXT_RIGHT_MARGIN; 2235 } 2236 } 2237 2238 private void setupAllDayTextRect(Rect r) { 2239 if (r.bottom <= r.top || r.right <= r.left) { 2240 r.bottom = r.top; 2241 r.right = r.left; 2242 return; 2243 } 2244 2245 if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { 2246 r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; 2247 r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; 2248 } 2249 if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { 2250 r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2251 r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2252 } 2253 } 2254 2255 /** 2256 * Return the layout for a numbered event. Create it if not already existing 2257 */ 2258 private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, 2259 Rect r) { 2260 if (i < 0 || i >= layouts.length) { 2261 return null; 2262 } 2263 2264 StaticLayout layout = layouts[i]; 2265 // Check if we have already initialized the StaticLayout and that 2266 // the width hasn't changed (due to vertical resizing which causes 2267 // re-layout of events at min height) 2268 if (layout == null || r.width() != layout.getWidth()) { 2269 String text = drawTextSanitizer(event.getTitleAndLocation(), MAX_EVENT_TEXT_LEN); 2270 2271 if (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) { 2272 paint.setColor(mDeclinedEventTextColor); 2273 } else { 2274 paint.setColor(mEventTextColor); 2275 } 2276 2277 // Leave a one pixel boundary on the left and right of the rectangle for the event 2278 layout = new StaticLayout(text, 0, text.length(), new TextPaint(paint), r.width(), 2279 Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); 2280 2281 layouts[i] = layout; 2282 } 2283 2284 return layout; 2285 } 2286 2287 private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { 2288 if (mSelectionAllDay) { 2289 // Draw the highlight on the selected all-day area 2290 mRect.top = DAY_HEADER_HEIGHT + 1; 2291 mRect.bottom = mRect.top + mAllDayHeight + ALLDAY_TOP_MARGIN - 2; 2292 int daynum = mSelectionDay - mFirstJulianDay; 2293 mRect.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP); 2294 mRect.right = mRect.left + mCellWidth + DAY_GAP - 1; 2295 p.setColor(mCalendarGridAreaSelected); 2296 canvas.drawRect(mRect, p); 2297 } 2298 2299 p.setTextSize(NORMAL_FONT_SIZE); 2300 p.setTextAlign(Paint.Align.LEFT); 2301 Paint eventTextPaint = mEventTextPaint; 2302 2303 // Draw the background for the all-day events area 2304 // r.top = DAY_HEADER_HEIGHT; 2305 // r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN; 2306 // r.left = mHoursWidth; 2307 // r.right = r.left + mNumDays * (mCellWidth + DAY_GAP); 2308 // p.setColor(mCalendarAllDayBackground); 2309 // canvas.drawRect(r, p); 2310 2311 // Fill the extra space on the right side with the default background 2312 // r.left = r.right; 2313 // r.right = mViewWidth; 2314 // p.setColor(mCalendarGridAreaBackground); 2315 // canvas.drawRect(r, p); 2316 2317 // Draw the outer vertical grid lines 2318 p.setColor(mCalendarGridLineVerticalColor); 2319 p.setStyle(Style.FILL); 2320 p.setStrokeWidth(GRID_LINE_WIDTH); 2321 p.setAntiAlias(false); 2322 final float startY = DAY_HEADER_HEIGHT; 2323 final float stopY = startY + mAllDayHeight + ALLDAY_TOP_MARGIN; 2324 final float deltaX = mCellWidth + DAY_GAP; 2325 float x = mHoursWidth; 2326 int linesIndex = 0; 2327 // Line bounding the top of the all day area 2328 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2329 mLines[linesIndex++] = startY; 2330 mLines[linesIndex++] = mHoursWidth + deltaX * mNumDays; 2331 mLines[linesIndex++] = startY; 2332 2333 for (int day = 0; day < mNumDays; day++) { 2334 x += deltaX; 2335 mLines[linesIndex++] = x; 2336 mLines[linesIndex++] = startY; 2337 mLines[linesIndex++] = x; 2338 mLines[linesIndex++] = stopY; 2339 } 2340 canvas.drawLines(mLines, 0, linesIndex, p); 2341 2342 // Draw the inner vertical grid lines 2343 p.setColor(mCalendarGridLineInnerVerticalColor); 2344 x = mHoursWidth; 2345 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2346 linesIndex = 0; 2347 // Line bounding the top of the all day area 2348 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2349 mLines[linesIndex++] = startY; 2350 mLines[linesIndex++] = mHoursWidth + (deltaX) * mNumDays; 2351 mLines[linesIndex++] = startY; 2352 2353 for (int day = 0; day < mNumDays; day++) { 2354 x += deltaX; 2355 mLines[linesIndex++] = x; 2356 mLines[linesIndex++] = startY; 2357 mLines[linesIndex++] = x; 2358 mLines[linesIndex++] = stopY; 2359 } 2360 canvas.drawLines(mLines, 0, linesIndex, p); 2361 2362 p.setAntiAlias(true); 2363 p.setStyle(Style.FILL); 2364 2365 int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 2366 float left = mHoursWidth; 2367 int lastDay = firstDay + numDays - 1; 2368 final ArrayList<Event> events = mAllDayEvents; 2369 int numEvents = events.size(); 2370 float drawHeight = mAllDayHeight; 2371 float numRectangles = mMaxAllDayEvents; 2372 for (int i = 0; i < numEvents; i++) { 2373 Event event = events.get(i); 2374 int startDay = event.startDay; 2375 int endDay = event.endDay; 2376 if (startDay > lastDay || endDay < firstDay) { 2377 continue; 2378 } 2379 if (startDay < firstDay) { 2380 startDay = firstDay; 2381 } 2382 if (endDay > lastDay) { 2383 endDay = lastDay; 2384 } 2385 int startIndex = startDay - firstDay; 2386 int endIndex = endDay - firstDay; 2387 float height = drawHeight / numRectangles; 2388 2389 // Prevent a single event from getting too big 2390 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 2391 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 2392 } 2393 2394 // Leave a one-pixel space between the vertical day lines and the 2395 // event rectangle. 2396 event.left = left + startIndex * (mCellWidth + DAY_GAP); 2397 event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth; 2398 event.top = y + height * event.getColumn(); 2399 event.bottom = event.top + height; 2400 2401 Rect r = drawEventRect(event, canvas, p, eventTextPaint); 2402 setupAllDayTextRect(r); 2403 StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); 2404 drawEventText(layout, r, canvas, r.top, r.bottom); 2405 2406 // Check if this all-day event intersects the selected day 2407 if (mSelectionAllDay && mComputeSelectedEvents) { 2408 if (startDay <= mSelectionDay && endDay >= mSelectionDay) { 2409 mSelectedEvents.add(event); 2410 } 2411 } 2412 } 2413 2414 if (mSelectionAllDay) { 2415 // Compute the neighbors for the list of all-day events that 2416 // intersect the selected day. 2417 computeAllDayNeighbors(); 2418 2419 // Set the selection position to zero so that when we move down 2420 // to the normal event area, we will highlight the topmost event. 2421 saveSelectionPosition(0f, 0f, 0f, 0f); 2422 } 2423 } 2424 2425 private void computeAllDayNeighbors() { 2426 int len = mSelectedEvents.size(); 2427 if (len == 0 || mSelectedEvent != null) { 2428 return; 2429 } 2430 2431 // First, clear all the links 2432 for (int ii = 0; ii < len; ii++) { 2433 Event ev = mSelectedEvents.get(ii); 2434 ev.nextUp = null; 2435 ev.nextDown = null; 2436 ev.nextLeft = null; 2437 ev.nextRight = null; 2438 } 2439 2440 // For each event in the selected event list "mSelectedEvents", find 2441 // its neighbors in the up and down directions. This could be done 2442 // more efficiently by sorting on the Event.getColumn() field, but 2443 // the list is expected to be very small. 2444 2445 // Find the event in the same row as the previously selected all-day 2446 // event, if any. 2447 int startPosition = -1; 2448 if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllDay()) { 2449 startPosition = mPrevSelectedEvent.getColumn(); 2450 } 2451 int maxPosition = -1; 2452 Event startEvent = null; 2453 Event maxPositionEvent = null; 2454 for (int ii = 0; ii < len; ii++) { 2455 Event ev = mSelectedEvents.get(ii); 2456 int position = ev.getColumn(); 2457 if (position == startPosition) { 2458 startEvent = ev; 2459 } else if (position > maxPosition) { 2460 maxPositionEvent = ev; 2461 maxPosition = position; 2462 } 2463 for (int jj = 0; jj < len; jj++) { 2464 if (jj == ii) { 2465 continue; 2466 } 2467 Event neighbor = mSelectedEvents.get(jj); 2468 int neighborPosition = neighbor.getColumn(); 2469 if (neighborPosition == position - 1) { 2470 ev.nextUp = neighbor; 2471 } else if (neighborPosition == position + 1) { 2472 ev.nextDown = neighbor; 2473 } 2474 } 2475 } 2476 if (startEvent != null) { 2477 mSelectedEvent = startEvent; 2478 } else { 2479 mSelectedEvent = maxPositionEvent; 2480 } 2481 } 2482 2483 private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) { 2484 Paint eventTextPaint = mEventTextPaint; 2485 int cellWidth = mCellWidth; 2486 int cellHeight = mCellHeight; 2487 2488 // Use the selected hour as the selection region 2489 Rect selectionArea = mSelectionRect; 2490 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); 2491 selectionArea.bottom = selectionArea.top + cellHeight; 2492 selectionArea.left = left; 2493 selectionArea.right = selectionArea.left + cellWidth; 2494 2495 final ArrayList<Event> events = mEvents; 2496 int numEvents = events.size(); 2497 EventGeometry geometry = mEventGeometry; 2498 2499 final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAllDayHeight; 2500 for (int i = 0; i < numEvents; i++) { 2501 Event event = events.get(i); 2502 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 2503 continue; 2504 } 2505 2506 // Don't draw it if it is not visible 2507 if (event.bottom < mViewStartY || event.top > viewEndY) { 2508 continue; 2509 } 2510 2511 if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents 2512 && geometry.eventIntersectsSelection(event, selectionArea)) { 2513 mSelectedEvents.add(event); 2514 } 2515 2516 Rect r = drawEventRect(event, canvas, p, eventTextPaint); 2517 setupTextRect(r); 2518 2519 // Don't draw text if it is not visible 2520 if (r.top > viewEndY || r.bottom < mViewStartY) { 2521 continue; 2522 } 2523 StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); 2524 // TODO: not sure why we are 4 pixels off 2525 drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight 2526 - DAY_HEADER_HEIGHT - mAllDayHeight); 2527 } 2528 2529 2530 if (mComputeSelectedEvents) { 2531 mDispatchAccessibilityEventRunnable.mEventType = mSelectedEvent == null ? 2532 AccessibilityEvent.TYPE_VIEW_SELECTED : 2533 AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED; 2534 post(mDispatchAccessibilityEventRunnable); 2535 } 2536 2537 if (date == mSelectionDay && !mSelectionAllDay && isFocused() 2538 && mSelectionMode != SELECTION_HIDDEN) { 2539 computeNeighbors(); 2540 } 2541 } 2542 2543 // Computes the "nearest" neighbor event in four directions (left, right, 2544 // up, down) for each of the events in the mSelectedEvents array. 2545 private void computeNeighbors() { 2546 int len = mSelectedEvents.size(); 2547 if (len == 0 || mSelectedEvent != null) { 2548 return; 2549 } 2550 2551 // First, clear all the links 2552 for (int ii = 0; ii < len; ii++) { 2553 Event ev = mSelectedEvents.get(ii); 2554 ev.nextUp = null; 2555 ev.nextDown = null; 2556 ev.nextLeft = null; 2557 ev.nextRight = null; 2558 } 2559 2560 Event startEvent = mSelectedEvents.get(0); 2561 int startEventDistance1 = 100000; // any large number 2562 int startEventDistance2 = 100000; // any large number 2563 int prevLocation = FROM_NONE; 2564 int prevTop; 2565 int prevBottom; 2566 int prevLeft; 2567 int prevRight; 2568 int prevCenter = 0; 2569 Rect box = getCurrentSelectionPosition(); 2570 if (mPrevSelectedEvent != null) { 2571 prevTop = (int) mPrevSelectedEvent.top; 2572 prevBottom = (int) mPrevSelectedEvent.bottom; 2573 prevLeft = (int) mPrevSelectedEvent.left; 2574 prevRight = (int) mPrevSelectedEvent.right; 2575 // Check if the previously selected event intersects the previous 2576 // selection box. (The previously selected event may be from a 2577 // much older selection box.) 2578 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top 2579 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { 2580 mPrevSelectedEvent = null; 2581 prevTop = mPrevBox.top; 2582 prevBottom = mPrevBox.bottom; 2583 prevLeft = mPrevBox.left; 2584 prevRight = mPrevBox.right; 2585 } else { 2586 // Clip the top and bottom to the previous selection box. 2587 if (prevTop < mPrevBox.top) { 2588 prevTop = mPrevBox.top; 2589 } 2590 if (prevBottom > mPrevBox.bottom) { 2591 prevBottom = mPrevBox.bottom; 2592 } 2593 } 2594 } else { 2595 // Just use the previously drawn selection box 2596 prevTop = mPrevBox.top; 2597 prevBottom = mPrevBox.bottom; 2598 prevLeft = mPrevBox.left; 2599 prevRight = mPrevBox.right; 2600 } 2601 2602 // Figure out where we came from and compute the center of that area. 2603 if (prevLeft >= box.right) { 2604 // The previously selected event was to the right of us. 2605 prevLocation = FROM_RIGHT; 2606 prevCenter = (prevTop + prevBottom) / 2; 2607 } else if (prevRight <= box.left) { 2608 // The previously selected event was to the left of us. 2609 prevLocation = FROM_LEFT; 2610 prevCenter = (prevTop + prevBottom) / 2; 2611 } else if (prevBottom <= box.top) { 2612 // The previously selected event was above us. 2613 prevLocation = FROM_ABOVE; 2614 prevCenter = (prevLeft + prevRight) / 2; 2615 } else if (prevTop >= box.bottom) { 2616 // The previously selected event was below us. 2617 prevLocation = FROM_BELOW; 2618 prevCenter = (prevLeft + prevRight) / 2; 2619 } 2620 2621 // For each event in the selected event list "mSelectedEvents", search 2622 // all the other events in that list for the nearest neighbor in 4 2623 // directions. 2624 for (int ii = 0; ii < len; ii++) { 2625 Event ev = mSelectedEvents.get(ii); 2626 2627 int startTime = ev.startTime; 2628 int endTime = ev.endTime; 2629 int left = (int) ev.left; 2630 int right = (int) ev.right; 2631 int top = (int) ev.top; 2632 if (top < box.top) { 2633 top = box.top; 2634 } 2635 int bottom = (int) ev.bottom; 2636 if (bottom > box.bottom) { 2637 bottom = box.bottom; 2638 } 2639 if (false) { 2640 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 2641 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 2642 if (DateFormat.is24HourFormat(mContext)) { 2643 flags |= DateUtils.FORMAT_24HOUR; 2644 } 2645 String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis, 2646 ev.endMillis, flags); 2647 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: " 2648 + bottom + " ev: " + timeRange + " " + ev.title); 2649 } 2650 int upDistanceMin = 10000; // any large number 2651 int downDistanceMin = 10000; // any large number 2652 int leftDistanceMin = 10000; // any large number 2653 int rightDistanceMin = 10000; // any large number 2654 Event upEvent = null; 2655 Event downEvent = null; 2656 Event leftEvent = null; 2657 Event rightEvent = null; 2658 2659 // Pick the starting event closest to the previously selected event, 2660 // if any. distance1 takes precedence over distance2. 2661 int distance1 = 0; 2662 int distance2 = 0; 2663 if (prevLocation == FROM_ABOVE) { 2664 if (left >= prevCenter) { 2665 distance1 = left - prevCenter; 2666 } else if (right <= prevCenter) { 2667 distance1 = prevCenter - right; 2668 } 2669 distance2 = top - prevBottom; 2670 } else if (prevLocation == FROM_BELOW) { 2671 if (left >= prevCenter) { 2672 distance1 = left - prevCenter; 2673 } else if (right <= prevCenter) { 2674 distance1 = prevCenter - right; 2675 } 2676 distance2 = prevTop - bottom; 2677 } else if (prevLocation == FROM_LEFT) { 2678 if (bottom <= prevCenter) { 2679 distance1 = prevCenter - bottom; 2680 } else if (top >= prevCenter) { 2681 distance1 = top - prevCenter; 2682 } 2683 distance2 = left - prevRight; 2684 } else if (prevLocation == FROM_RIGHT) { 2685 if (bottom <= prevCenter) { 2686 distance1 = prevCenter - bottom; 2687 } else if (top >= prevCenter) { 2688 distance1 = top - prevCenter; 2689 } 2690 distance2 = prevLeft - right; 2691 } 2692 if (distance1 < startEventDistance1 2693 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { 2694 startEvent = ev; 2695 startEventDistance1 = distance1; 2696 startEventDistance2 = distance2; 2697 } 2698 2699 // For each neighbor, figure out if it is above or below or left 2700 // or right of me and compute the distance. 2701 for (int jj = 0; jj < len; jj++) { 2702 if (jj == ii) { 2703 continue; 2704 } 2705 Event neighbor = mSelectedEvents.get(jj); 2706 int neighborLeft = (int) neighbor.left; 2707 int neighborRight = (int) neighbor.right; 2708 if (neighbor.endTime <= startTime) { 2709 // This neighbor is entirely above me. 2710 // If we overlap the same column, then compute the distance. 2711 if (neighborLeft < right && neighborRight > left) { 2712 int distance = startTime - neighbor.endTime; 2713 if (distance < upDistanceMin) { 2714 upDistanceMin = distance; 2715 upEvent = neighbor; 2716 } else if (distance == upDistanceMin) { 2717 int center = (left + right) / 2; 2718 int currentDistance = 0; 2719 int currentLeft = (int) upEvent.left; 2720 int currentRight = (int) upEvent.right; 2721 if (currentRight <= center) { 2722 currentDistance = center - currentRight; 2723 } else if (currentLeft >= center) { 2724 currentDistance = currentLeft - center; 2725 } 2726 2727 int neighborDistance = 0; 2728 if (neighborRight <= center) { 2729 neighborDistance = center - neighborRight; 2730 } else if (neighborLeft >= center) { 2731 neighborDistance = neighborLeft - center; 2732 } 2733 if (neighborDistance < currentDistance) { 2734 upDistanceMin = distance; 2735 upEvent = neighbor; 2736 } 2737 } 2738 } 2739 } else if (neighbor.startTime >= endTime) { 2740 // This neighbor is entirely below me. 2741 // If we overlap the same column, then compute the distance. 2742 if (neighborLeft < right && neighborRight > left) { 2743 int distance = neighbor.startTime - endTime; 2744 if (distance < downDistanceMin) { 2745 downDistanceMin = distance; 2746 downEvent = neighbor; 2747 } else if (distance == downDistanceMin) { 2748 int center = (left + right) / 2; 2749 int currentDistance = 0; 2750 int currentLeft = (int) downEvent.left; 2751 int currentRight = (int) downEvent.right; 2752 if (currentRight <= center) { 2753 currentDistance = center - currentRight; 2754 } else if (currentLeft >= center) { 2755 currentDistance = currentLeft - center; 2756 } 2757 2758 int neighborDistance = 0; 2759 if (neighborRight <= center) { 2760 neighborDistance = center - neighborRight; 2761 } else if (neighborLeft >= center) { 2762 neighborDistance = neighborLeft - center; 2763 } 2764 if (neighborDistance < currentDistance) { 2765 downDistanceMin = distance; 2766 downEvent = neighbor; 2767 } 2768 } 2769 } 2770 } 2771 2772 if (neighborLeft >= right) { 2773 // This neighbor is entirely to the right of me. 2774 // Take the closest neighbor in the y direction. 2775 int center = (top + bottom) / 2; 2776 int distance = 0; 2777 int neighborBottom = (int) neighbor.bottom; 2778 int neighborTop = (int) neighbor.top; 2779 if (neighborBottom <= center) { 2780 distance = center - neighborBottom; 2781 } else if (neighborTop >= center) { 2782 distance = neighborTop - center; 2783 } 2784 if (distance < rightDistanceMin) { 2785 rightDistanceMin = distance; 2786 rightEvent = neighbor; 2787 } else if (distance == rightDistanceMin) { 2788 // Pick the closest in the x direction 2789 int neighborDistance = neighborLeft - right; 2790 int currentDistance = (int) rightEvent.left - right; 2791 if (neighborDistance < currentDistance) { 2792 rightDistanceMin = distance; 2793 rightEvent = neighbor; 2794 } 2795 } 2796 } else if (neighborRight <= left) { 2797 // This neighbor is entirely to the left of me. 2798 // Take the closest neighbor in the y direction. 2799 int center = (top + bottom) / 2; 2800 int distance = 0; 2801 int neighborBottom = (int) neighbor.bottom; 2802 int neighborTop = (int) neighbor.top; 2803 if (neighborBottom <= center) { 2804 distance = center - neighborBottom; 2805 } else if (neighborTop >= center) { 2806 distance = neighborTop - center; 2807 } 2808 if (distance < leftDistanceMin) { 2809 leftDistanceMin = distance; 2810 leftEvent = neighbor; 2811 } else if (distance == leftDistanceMin) { 2812 // Pick the closest in the x direction 2813 int neighborDistance = left - neighborRight; 2814 int currentDistance = left - (int) leftEvent.right; 2815 if (neighborDistance < currentDistance) { 2816 leftDistanceMin = distance; 2817 leftEvent = neighbor; 2818 } 2819 } 2820 } 2821 } 2822 ev.nextUp = upEvent; 2823 ev.nextDown = downEvent; 2824 ev.nextLeft = leftEvent; 2825 ev.nextRight = rightEvent; 2826 } 2827 mSelectedEvent = startEvent; 2828 } 2829 2830 private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) { 2831 // Draw the Event Rect 2832 Rect r = mRect; 2833 r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; 2834 r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; 2835 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 2836 r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; 2837 2838 Drawable eventBoxDrawable; 2839 switch (event.selfAttendeeStatus) { 2840 case Attendees.ATTENDEE_STATUS_INVITED: 2841 case Attendees.ATTENDEE_STATUS_DECLINED: 2842 eventBoxDrawable = mUnconfirmedOrDeclinedEventBoxDrawable; 2843 break; 2844 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 2845 case Attendees.ATTENDEE_STATUS_ACCEPTED: 2846 case Attendees.ATTENDEE_STATUS_TENTATIVE: 2847 default: 2848 eventBoxDrawable = mAcceptedOrTentativeEventBoxDrawable; 2849 break; 2850 } 2851 eventBoxDrawable.setBounds(r); 2852 eventBoxDrawable.draw(canvas); 2853 2854 p.setStyle(Style.FILL); 2855 2856 // If this event is selected, then use the selection color 2857 if (mSelectedEvent == event) { 2858 boolean paintIt = false; 2859 int color = 0; 2860 if (mSelectionMode == SELECTION_PRESSED) { 2861 // Also, remember the last selected event that we drew 2862 mPrevSelectedEvent = event; 2863 // box = mBoxPressed; 2864 color = mPressedColor; 2865 paintIt = true; 2866 } else if (mSelectionMode == SELECTION_SELECTED) { 2867 // Also, remember the last selected event that we drew 2868 mPrevSelectedEvent = event; 2869 // box = mBoxSelected; 2870 color = mPressedColor; 2871 paintIt = true; 2872 } else if (mSelectionMode == SELECTION_LONGPRESS) { 2873 // box = mBoxLongPressed; 2874 color = mPressedColor; 2875 paintIt = true; 2876 } 2877 2878 if (paintIt) { 2879 p.setColor(color); 2880 canvas.drawRect(r, p); 2881 } 2882 } 2883 2884 // Draw cal color square border 2885 r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; 2886 r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; 2887 r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; 2888 r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; 2889 p.setColor(0xFFFFFFFF); 2890 canvas.drawRect(r, p); 2891 2892 // Draw cal color 2893 r.top++; 2894 r.left++; 2895 r.bottom--; 2896 r.right--; 2897 p.setColor(event.color); 2898 canvas.drawRect(r, p); 2899 2900 // Setup rect for drawEventText which follows 2901 r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; 2902 r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; 2903 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 2904 r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; 2905 return r; 2906 } 2907 2908 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); 2909 2910 // Sanitize a string before passing it to drawText or else we get little 2911 // squares. For newlines and tabs before a comma, delete the character. 2912 // Otherwise, just replace them with a space. 2913 private String drawTextSanitizer(String string, int maxEventTextLen) { 2914 Matcher m = drawTextSanitizerFilter.matcher(string); 2915 string = m.replaceAll(","); 2916 2917 int len = string.length(); 2918 if (len > maxEventTextLen) { 2919 string = string.substring(0, maxEventTextLen); 2920 len = maxEventTextLen; 2921 } 2922 2923 return string.replace('\n', ' '); 2924 } 2925 2926 private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, 2927 int bottom) { 2928 // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging 2929 2930 int width = rect.right - rect.left; 2931 int height = rect.bottom - rect.top; 2932 2933 // If the rectangle is too small for text, then return 2934 if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { 2935 return; 2936 } 2937 2938 int totalLineHeight = 0; 2939 int lineCount = eventLayout.getLineCount(); 2940 for (int i = 0; i < lineCount; i++) { 2941 int lineBottom = eventLayout.getLineBottom(i); 2942 if (lineBottom <= height) { 2943 totalLineHeight = lineBottom; 2944 } else { 2945 break; 2946 } 2947 } 2948 2949 if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { 2950 return; 2951 } 2952 2953 // Use a StaticLayout to format the string. 2954 canvas.save(); 2955 canvas.translate(rect.left, rect.top); 2956 rect.left = 0; 2957 rect.right = width; 2958 rect.top = 0; 2959 rect.bottom = totalLineHeight; 2960 2961 // There's a bug somewhere. If this rect is outside of a previous 2962 // cliprect, this becomes a no-op. What happens is that the text draw 2963 // past the event rect. The current fix is to not draw the staticLayout 2964 // at all if it is completely out of bound. 2965 canvas.clipRect(rect); 2966 eventLayout.draw(canvas); 2967 canvas.restore(); 2968 } 2969 2970 // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it 2971 // doesn't work well with hardware acceleration 2972 private void drawEmptyRect(Canvas canvas, Rect r, int color) { 2973 int linesIndex = 0; 2974 mLines[linesIndex++] = r.left; 2975 mLines[linesIndex++] = r.top; 2976 mLines[linesIndex++] = r.right; 2977 mLines[linesIndex++] = r.top; 2978 2979 mLines[linesIndex++] = r.left; 2980 mLines[linesIndex++] = r.bottom; 2981 mLines[linesIndex++] = r.right; 2982 mLines[linesIndex++] = r.bottom; 2983 2984 mLines[linesIndex++] = r.left; 2985 mLines[linesIndex++] = r.top; 2986 mLines[linesIndex++] = r.left; 2987 mLines[linesIndex++] = r.bottom; 2988 2989 mLines[linesIndex++] = r.right; 2990 mLines[linesIndex++] = r.top; 2991 mLines[linesIndex++] = r.right; 2992 mLines[linesIndex++] = r.bottom; 2993 mPaint.setColor(color); 2994 canvas.drawLines(mLines, 0, linesIndex, mPaint); 2995 } 2996 2997 private void updateEventDetails() { 2998 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN 2999 || mSelectionMode == SELECTION_LONGPRESS) { 3000 mPopup.dismiss(); 3001 return; 3002 } 3003 if (mLastPopupEventID == mSelectedEvent.id) { 3004 return; 3005 } 3006 3007 mLastPopupEventID = mSelectedEvent.id; 3008 3009 // Remove any outstanding callbacks to dismiss the popup. 3010 getHandler().removeCallbacks(mDismissPopup); 3011 3012 Event event = mSelectedEvent; 3013 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); 3014 titleView.setText(event.title); 3015 3016 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); 3017 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); 3018 3019 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); 3020 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); 3021 3022 int flags; 3023 if (event.allDay) { 3024 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE 3025 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; 3026 } else { 3027 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 3028 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL 3029 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3030 } 3031 if (DateFormat.is24HourFormat(mContext)) { 3032 flags |= DateUtils.FORMAT_24HOUR; 3033 } 3034 String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis, 3035 flags); 3036 TextView timeView = (TextView) mPopupView.findViewById(R.id.time); 3037 timeView.setText(timeRange); 3038 3039 TextView whereView = (TextView) mPopupView.findViewById(R.id.where); 3040 final boolean empty = TextUtils.isEmpty(event.location); 3041 whereView.setVisibility(empty ? View.GONE : View.VISIBLE); 3042 if (!empty) whereView.setText(event.location); 3043 3044 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); 3045 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); 3046 } 3047 3048 // The following routines are called from the parent activity when certain 3049 // touch events occur. 3050 private void doDown(MotionEvent ev) { 3051 mTouchMode = TOUCH_MODE_DOWN; 3052 mViewStartX = 0; 3053 mOnFlingCalled = false; 3054 getHandler().removeCallbacks(mContinueScroll); 3055 } 3056 3057 private void doSingleTapUp(MotionEvent ev) { 3058 if (!mHandleActionUp) { 3059 return; 3060 } 3061 3062 int x = (int) ev.getX(); 3063 int y = (int) ev.getY(); 3064 int selectedDay = mSelectionDay; 3065 int selectedHour = mSelectionHour; 3066 3067 boolean validPosition = setSelectionFromPosition(x, y); 3068 if (!validPosition) { 3069 if (y < DAY_HEADER_HEIGHT) { 3070 Time selectedTime = new Time(mBaseDate); 3071 selectedTime.setJulianDay(mSelectionDay); 3072 selectedTime.hour = mSelectionHour; 3073 selectedTime.normalize(true /* ignore isDst */); 3074 mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, 3075 ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); 3076 } 3077 return; 3078 } 3079 3080 mSelectionMode = SELECTION_SELECTED; 3081 invalidate(); 3082 3083 if (mSelectedEvent != null) { 3084 // If the tap is on an event, launch the "View event" view 3085 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mSelectedEvent.id, 3086 mSelectedEvent.startMillis, mSelectedEvent.endMillis, (int) ev.getRawX(), 3087 (int) ev.getRawY(), getSelectedTimeInMillis()); 3088 } else if (selectedDay == mSelectionDay && selectedHour == mSelectionHour) { 3089 // If the tap is on an already selected hour slot, then create a new 3090 // event 3091 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 3092 getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), -1); 3093 } else { 3094 Time startTime = new Time(mBaseDate); 3095 startTime.setJulianDay(mSelectionDay); 3096 startTime.hour = mSelectionHour; 3097 startTime.normalize(true /* ignore isDst */); 3098 3099 Time endTime = new Time(startTime); 3100 endTime.hour++; 3101 3102 mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, 3103 CalendarController.EXTRA_GOTO_TIME, null, null); 3104 } 3105 } 3106 3107 private void doLongPress(MotionEvent ev) { 3108 // Scale gesture in progress 3109 if (mStartingSpanY != 0) { 3110 return; 3111 } 3112 3113 int x = (int) ev.getX(); 3114 int y = (int) ev.getY(); 3115 3116 boolean validPosition = setSelectionFromPosition(x, y); 3117 if (!validPosition) { 3118 // return if the touch wasn't on an area of concern 3119 return; 3120 } 3121 3122 mSelectionMode = SELECTION_LONGPRESS; 3123 invalidate(); 3124 performLongClick(); 3125 } 3126 3127 private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { 3128 if (isAnimating()) { 3129 return; 3130 } 3131 3132 // Use the distance from the current point to the initial touch instead 3133 // of deltaX and deltaY to avoid accumulating floating-point rounding 3134 // errors. Also, we don't need floats, we can use ints. 3135 int distanceX = (int) e1.getX() - (int) e2.getX(); 3136 int distanceY = (int) e1.getY() - (int) e2.getY(); 3137 3138 // If we haven't figured out the predominant scroll direction yet, 3139 // then do it now. 3140 if (mTouchMode == TOUCH_MODE_DOWN) { 3141 int absDistanceX = Math.abs(distanceX); 3142 int absDistanceY = Math.abs(distanceY); 3143 mScrollStartY = mViewStartY; 3144 mPreviousDirection = 0; 3145 3146 // If the x distance is at least twice the y distance, then lock 3147 // the scroll horizontally. Otherwise scroll vertically. 3148 if (absDistanceX >= 2 * absDistanceY) { 3149 mTouchMode = TOUCH_MODE_HSCROLL; 3150 mViewStartX = distanceX; 3151 initNextView(-mViewStartX); 3152 } else { 3153 mTouchMode = TOUCH_MODE_VSCROLL; 3154 } 3155 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3156 // We are already scrolling horizontally, so check if we 3157 // changed the direction of scrolling so that the other week 3158 // is now visible. 3159 mViewStartX = distanceX; 3160 if (distanceX != 0) { 3161 int direction = (distanceX > 0) ? 1 : -1; 3162 if (direction != mPreviousDirection) { 3163 // The user has switched the direction of scrolling 3164 // so re-init the next view 3165 initNextView(-mViewStartX); 3166 mPreviousDirection = direction; 3167 } 3168 } 3169 } 3170 3171 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { 3172 mViewStartY = mScrollStartY + distanceY; 3173 if (mViewStartY < 0) { 3174 mViewStartY = 0; 3175 } else if (mViewStartY > mMaxViewStartY) { 3176 mViewStartY = mMaxViewStartY; 3177 } 3178 computeFirstHour(); 3179 } 3180 3181 mScrolling = true; 3182 3183 mSelectionMode = SELECTION_HIDDEN; 3184 invalidate(); 3185 } 3186 3187 private boolean isAnimating() { 3188 Animation in = mViewSwitcher.getInAnimation(); 3189 if (in != null && in.hasStarted() && !in.hasEnded()) { 3190 return true; 3191 } 3192 Animation out = mViewSwitcher.getOutAnimation(); 3193 if (out != null && out.hasStarted() && !out.hasEnded()) { 3194 return true; 3195 } 3196 return false; 3197 } 3198 3199 private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 3200 if (isAnimating()) { 3201 return; 3202 } 3203 3204 mTouchMode = TOUCH_MODE_INITIAL_STATE; 3205 mSelectionMode = SELECTION_HIDDEN; 3206 mOnFlingCalled = true; 3207 int deltaX = (int) e2.getX() - (int) e1.getX(); 3208 int distanceX = Math.abs(deltaX); 3209 int deltaY = (int) e2.getY() - (int) e1.getY(); 3210 int distanceY = Math.abs(deltaY); 3211 if (DEBUG) Log.d(TAG, "doFling: deltaX " + deltaX 3212 + ", HORIZONTAL_FLING_THRESHOLD " + HORIZONTAL_FLING_THRESHOLD); 3213 3214 if ((distanceX >= HORIZONTAL_FLING_THRESHOLD) && (distanceX > distanceY)) { 3215 // Horizontal fling. 3216 // initNextView(deltaX); 3217 switchViews(deltaX < 0, mViewStartX, mViewWidth); 3218 mViewStartX = 0; 3219 return; 3220 } 3221 3222 // Vertical fling. 3223 mViewStartX = 0; 3224 3225 if (DEBUG) { 3226 Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); 3227 } 3228 3229 // Continue scrolling vertically 3230 mScrolling = true; 3231 mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, 3232 (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, mMaxViewStartY); 3233 post(mContinueScroll); 3234 } 3235 3236 private boolean initNextView(int deltaX) { 3237 // Change the view to the previous day or week 3238 DayView view = (DayView) mViewSwitcher.getNextView(); 3239 Time date = view.mBaseDate; 3240 date.set(mBaseDate); 3241 boolean switchForward; 3242 if (deltaX > 0) { 3243 date.monthDay -= mNumDays; 3244 view.mSelectionDay = mSelectionDay - mNumDays; 3245 switchForward = false; 3246 } else { 3247 date.monthDay += mNumDays; 3248 view.mSelectionDay = mSelectionDay + mNumDays; 3249 switchForward = true; 3250 } 3251 date.normalize(true /* ignore isDst */); 3252 initView(view); 3253 view.layout(getLeft(), getTop(), getRight(), getBottom()); 3254 view.reloadEvents(); 3255 return switchForward; 3256 } 3257 3258 // ScaleGestureDetector.OnScaleGestureListener 3259 public boolean onScaleBegin(ScaleGestureDetector detector) { 3260 mHandleActionUp = false; 3261 float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAllDayHeight; 3262 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); 3263 3264 mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 3265 mCellHeightBeforeScaleGesture = mCellHeight; 3266 3267 if (DEBUG) { 3268 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 3269 Log.d(TAG, "mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " 3270 + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" 3271 + mCellHeight); 3272 } 3273 3274 return true; 3275 } 3276 3277 // ScaleGestureDetector.OnScaleGestureListener 3278 public boolean onScale(ScaleGestureDetector detector) { 3279 float spanY = Math.abs(detector.getCurrentSpanY()); 3280 3281 mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); 3282 3283 if (mCellHeight < mMinCellHeight) { 3284 // If mStartingSpanY is too small, even a small increase in the 3285 // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT 3286 mStartingSpanY = Math.max(MIN_Y_SPAN, spanY); 3287 mCellHeight = mMinCellHeight; 3288 mCellHeightBeforeScaleGesture = mMinCellHeight; 3289 } else if (mCellHeight > MAX_CELL_HEIGHT) { 3290 mStartingSpanY = spanY; 3291 mCellHeight = MAX_CELL_HEIGHT; 3292 mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; 3293 } 3294 3295 int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAllDayHeight; 3296 mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; 3297 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 3298 3299 if (DEBUG) { 3300 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 3301 Log.d(TAG, " mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " 3302 + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" 3303 + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 3304 } 3305 3306 if (mViewStartY < 0) { 3307 mViewStartY = 0; 3308 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 3309 / (float) (mCellHeight + DAY_GAP); 3310 } else if (mViewStartY > mMaxViewStartY) { 3311 mViewStartY = mMaxViewStartY; 3312 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 3313 / (float) (mCellHeight + DAY_GAP); 3314 } 3315 computeFirstHour(); 3316 3317 mRemeasure = true; 3318 invalidate(); 3319 return true; 3320 } 3321 3322 // ScaleGestureDetector.OnScaleGestureListener 3323 public void onScaleEnd(ScaleGestureDetector detector) { 3324 mStartingSpanY = 0; 3325 } 3326 3327 @Override 3328 public boolean onTouchEvent(MotionEvent ev) { 3329 int action = ev.getAction(); 3330 3331 if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { 3332 mScaleGestureDetector.onTouchEvent(ev); 3333 if (mScaleGestureDetector.isInProgress()) { 3334 return true; 3335 } 3336 } 3337 3338 switch (action) { 3339 case MotionEvent.ACTION_DOWN: 3340 if (DEBUG) Log.e(TAG, "ACTION_DOWN"); 3341 mHandleActionUp = true; 3342 mGestureDetector.onTouchEvent(ev); 3343 return true; 3344 3345 case MotionEvent.ACTION_MOVE: 3346 if (DEBUG) Log.e(TAG, "ACTION_MOVE"); 3347 mGestureDetector.onTouchEvent(ev); 3348 return true; 3349 3350 case MotionEvent.ACTION_UP: 3351 if (DEBUG) Log.e(TAG, "ACTION_UP " + mHandleActionUp); 3352 mGestureDetector.onTouchEvent(ev); 3353 if (!mHandleActionUp) { 3354 mHandleActionUp = true; 3355 return true; 3356 } 3357 if (mOnFlingCalled) { 3358 return true; 3359 } 3360 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3361 mTouchMode = TOUCH_MODE_INITIAL_STATE; 3362 if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { 3363 // The user has gone beyond the threshold so switch views 3364 if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); 3365 switchViews(mViewStartX > 0, mViewStartX, mViewWidth); 3366 mViewStartX = 0; 3367 return true; 3368 } else { 3369 // Not beyond the threshold so invalidate which will cause 3370 // the view to snap back. Also call recalc() to ensure 3371 // that we have the correct starting date and title. 3372 if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); 3373 recalc(); 3374 invalidate(); 3375 mViewStartX = 0; 3376 } 3377 } 3378 3379 // If we were scrolling, then reset the selected hour so that it 3380 // is visible. 3381 if (mScrolling) { 3382 mScrolling = false; 3383 resetSelectedHour(); 3384 invalidate(); 3385 } 3386 return true; 3387 3388 // This case isn't expected to happen. 3389 case MotionEvent.ACTION_CANCEL: 3390 if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); 3391 mGestureDetector.onTouchEvent(ev); 3392 mScrolling = false; 3393 resetSelectedHour(); 3394 return true; 3395 3396 default: 3397 if (DEBUG) Log.e(TAG, "Not MotionEvent"); 3398 if (mGestureDetector.onTouchEvent(ev)) { 3399 return true; 3400 } 3401 return super.onTouchEvent(ev); 3402 } 3403 } 3404 3405 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 3406 MenuItem item; 3407 3408 // If the trackball is held down, then the context menu pops up and 3409 // we never get onKeyUp() for the long-press. So check for it here 3410 // and change the selection to the long-press state. 3411 if (mSelectionMode != SELECTION_LONGPRESS) { 3412 mSelectionMode = SELECTION_LONGPRESS; 3413 invalidate(); 3414 } 3415 3416 final long startMillis = getSelectedTimeInMillis(); 3417 int flags = DateUtils.FORMAT_SHOW_TIME 3418 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT 3419 | DateUtils.FORMAT_SHOW_WEEKDAY; 3420 final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); 3421 menu.setHeaderTitle(title); 3422 3423 int numSelectedEvents = mSelectedEvents.size(); 3424 if (mNumDays == 1) { 3425 // Day view. 3426 3427 // If there is a selected event, then allow it to be viewed and 3428 // edited. 3429 if (numSelectedEvents >= 1) { 3430 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 3431 item.setOnMenuItemClickListener(mContextMenuHandler); 3432 item.setIcon(android.R.drawable.ic_menu_info_details); 3433 3434 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 3435 if (accessLevel == ACCESS_LEVEL_EDIT) { 3436 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 3437 item.setOnMenuItemClickListener(mContextMenuHandler); 3438 item.setIcon(android.R.drawable.ic_menu_edit); 3439 item.setAlphabeticShortcut('e'); 3440 } 3441 3442 if (accessLevel >= ACCESS_LEVEL_DELETE) { 3443 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 3444 item.setOnMenuItemClickListener(mContextMenuHandler); 3445 item.setIcon(android.R.drawable.ic_menu_delete); 3446 } 3447 3448 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 3449 item.setOnMenuItemClickListener(mContextMenuHandler); 3450 item.setIcon(android.R.drawable.ic_menu_add); 3451 item.setAlphabeticShortcut('n'); 3452 } else { 3453 // Otherwise, if the user long-pressed on a blank hour, allow 3454 // them to create an event. They can also do this by tapping. 3455 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 3456 item.setOnMenuItemClickListener(mContextMenuHandler); 3457 item.setIcon(android.R.drawable.ic_menu_add); 3458 item.setAlphabeticShortcut('n'); 3459 } 3460 } else { 3461 // Week view. 3462 3463 // If there is a selected event, then allow it to be viewed and 3464 // edited. 3465 if (numSelectedEvents >= 1) { 3466 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 3467 item.setOnMenuItemClickListener(mContextMenuHandler); 3468 item.setIcon(android.R.drawable.ic_menu_info_details); 3469 3470 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 3471 if (accessLevel == ACCESS_LEVEL_EDIT) { 3472 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 3473 item.setOnMenuItemClickListener(mContextMenuHandler); 3474 item.setIcon(android.R.drawable.ic_menu_edit); 3475 item.setAlphabeticShortcut('e'); 3476 } 3477 3478 if (accessLevel >= ACCESS_LEVEL_DELETE) { 3479 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 3480 item.setOnMenuItemClickListener(mContextMenuHandler); 3481 item.setIcon(android.R.drawable.ic_menu_delete); 3482 } 3483 } 3484 3485 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 3486 item.setOnMenuItemClickListener(mContextMenuHandler); 3487 item.setIcon(android.R.drawable.ic_menu_add); 3488 item.setAlphabeticShortcut('n'); 3489 3490 item = menu.add(0, MENU_DAY, 0, R.string.show_day_view); 3491 item.setOnMenuItemClickListener(mContextMenuHandler); 3492 item.setIcon(android.R.drawable.ic_menu_day); 3493 item.setAlphabeticShortcut('d'); 3494 } 3495 3496 mPopup.dismiss(); 3497 } 3498 3499 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { 3500 public boolean onMenuItemClick(MenuItem item) { 3501 switch (item.getItemId()) { 3502 case MENU_EVENT_VIEW: { 3503 if (mSelectedEvent != null) { 3504 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, 3505 mSelectedEvent.id, mSelectedEvent.startMillis, 3506 mSelectedEvent.endMillis, 0, 0, -1); 3507 } 3508 break; 3509 } 3510 case MENU_EVENT_EDIT: { 3511 if (mSelectedEvent != null) { 3512 mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, 3513 mSelectedEvent.id, mSelectedEvent.startMillis, 3514 mSelectedEvent.endMillis, 0, 0, -1); 3515 } 3516 break; 3517 } 3518 case MENU_DAY: { 3519 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 3520 ViewType.DAY); 3521 break; 3522 } 3523 case MENU_AGENDA: { 3524 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 3525 ViewType.AGENDA); 3526 break; 3527 } 3528 case MENU_EVENT_CREATE: { 3529 long startMillis = getSelectedTimeInMillis(); 3530 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 3531 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 3532 startMillis, endMillis, 0, 0, -1); 3533 break; 3534 } 3535 case MENU_EVENT_DELETE: { 3536 if (mSelectedEvent != null) { 3537 Event selectedEvent = mSelectedEvent; 3538 long begin = selectedEvent.startMillis; 3539 long end = selectedEvent.endMillis; 3540 long id = selectedEvent.id; 3541 mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin, 3542 end, 0, 0, -1); 3543 } 3544 break; 3545 } 3546 default: { 3547 return false; 3548 } 3549 } 3550 return true; 3551 } 3552 } 3553 3554 private static int getEventAccessLevel(Context context, Event e) { 3555 ContentResolver cr = context.getContentResolver(); 3556 3557 int visibility = Calendars.NO_ACCESS; 3558 3559 // Get the calendar id for this event 3560 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id), 3561 new String[] { Events.CALENDAR_ID }, 3562 null /* selection */, 3563 null /* selectionArgs */, 3564 null /* sort */); 3565 3566 if (cursor == null) { 3567 return ACCESS_LEVEL_NONE; 3568 } 3569 3570 if (cursor.getCount() == 0) { 3571 cursor.close(); 3572 return ACCESS_LEVEL_NONE; 3573 } 3574 3575 cursor.moveToFirst(); 3576 long calId = cursor.getLong(0); 3577 cursor.close(); 3578 3579 Uri uri = Calendars.CONTENT_URI; 3580 String where = String.format(CALENDARS_WHERE, calId); 3581 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null); 3582 3583 String calendarOwnerAccount = null; 3584 if (cursor != null) { 3585 cursor.moveToFirst(); 3586 visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); 3587 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 3588 cursor.close(); 3589 } 3590 3591 if (visibility < Calendars.CONTRIBUTOR_ACCESS) { 3592 return ACCESS_LEVEL_NONE; 3593 } 3594 3595 if (e.guestsCanModify) { 3596 return ACCESS_LEVEL_EDIT; 3597 } 3598 3599 if (!TextUtils.isEmpty(calendarOwnerAccount) 3600 && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { 3601 return ACCESS_LEVEL_EDIT; 3602 } 3603 3604 return ACCESS_LEVEL_DELETE; 3605 } 3606 3607 /** 3608 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. 3609 * If the touch position is not within the displayed grid, then this 3610 * method returns false. 3611 * 3612 * @param x the x position of the touch 3613 * @param y the y position of the touch 3614 * @return true if the touch position is valid 3615 */ 3616 private boolean setSelectionFromPosition(int x, final int y) { 3617 if (x < mHoursWidth) { 3618 x = mHoursWidth; 3619 } 3620 3621 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); 3622 if (day >= mNumDays) { 3623 day = mNumDays - 1; 3624 } 3625 day += mFirstJulianDay; 3626 mSelectionDay = day; 3627 3628 if (y < DAY_HEADER_HEIGHT) { 3629 return false; 3630 } 3631 3632 mSelectionHour = mFirstHour; /* First fully visible hour */ 3633 3634 if (y < mFirstCell) { 3635 mSelectionAllDay = true; 3636 } else { 3637 // y is now offset from top of the scrollable region 3638 int adjustedY = y - mFirstCell; 3639 3640 if (adjustedY < mFirstHourOffset) { 3641 --mSelectionHour; /* In the partially visible hour */ 3642 } else { 3643 mSelectionHour += (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP); 3644 } 3645 3646 mSelectionAllDay = false; 3647 } 3648 3649 findSelectedEvent(x, y); 3650 3651// Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: " 3652// + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " 3653// + mFirstHourOffset); 3654// if (mSelectedEvent != null) { 3655// Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " 3656// + mSelectedEvent.title); 3657// for (Event ev : mSelectedEvents) { 3658// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 3659// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3660// String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags); 3661// 3662// Log.i("Cal", " " + timeRange + " " + ev.title); 3663// } 3664// } 3665 return true; 3666 } 3667 3668 private void findSelectedEvent(int x, int y) { 3669 int date = mSelectionDay; 3670 int cellWidth = mCellWidth; 3671 final ArrayList<Event> events = mEvents; 3672 int numEvents = events.size(); 3673 int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP); 3674 int top = 0; 3675 mSelectedEvent = null; 3676 3677 mSelectedEvents.clear(); 3678 if (mSelectionAllDay) { 3679 float yDistance; 3680 float minYdistance = 10000.0f; // any large number 3681 Event closestEvent = null; 3682 float drawHeight = mAllDayHeight; 3683 int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 3684 for (int i = 0; i < numEvents; i++) { 3685 Event event = events.get(i); 3686 if (!event.drawAsAllDay()) { 3687 continue; 3688 } 3689 3690 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { 3691 float numRectangles = event.getMaxColumns(); 3692 float height = drawHeight / numRectangles; 3693 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 3694 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 3695 } 3696 float eventTop = yOffset + height * event.getColumn(); 3697 float eventBottom = eventTop + height; 3698 if (eventTop < y && eventBottom > y) { 3699 // If the touch is inside the event rectangle, then 3700 // add the event. 3701 mSelectedEvents.add(event); 3702 closestEvent = event; 3703 break; 3704 } else { 3705 // Find the closest event 3706 if (eventTop >= y) { 3707 yDistance = eventTop - y; 3708 } else { 3709 yDistance = y - eventBottom; 3710 } 3711 if (yDistance < minYdistance) { 3712 minYdistance = yDistance; 3713 closestEvent = event; 3714 } 3715 } 3716 } 3717 } 3718 mSelectedEvent = closestEvent; 3719 return; 3720 } 3721 3722 // Adjust y for the scrollable bitmap 3723 y += mViewStartY - mFirstCell; 3724 3725 // Use a region around (x,y) for the selection region 3726 Rect region = mRect; 3727 region.left = x - 10; 3728 region.right = x + 10; 3729 region.top = y - 10; 3730 region.bottom = y + 10; 3731 3732 EventGeometry geometry = mEventGeometry; 3733 3734 for (int i = 0; i < numEvents; i++) { 3735 Event event = events.get(i); 3736 // Compute the event rectangle. 3737 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 3738 continue; 3739 } 3740 3741 // If the event intersects the selection region, then add it to 3742 // mSelectedEvents. 3743 if (geometry.eventIntersectsSelection(event, region)) { 3744 mSelectedEvents.add(event); 3745 } 3746 } 3747 3748 // If there are any events in the selected region, then assign the 3749 // closest one to mSelectedEvent. 3750 if (mSelectedEvents.size() > 0) { 3751 int len = mSelectedEvents.size(); 3752 Event closestEvent = null; 3753 float minDist = mViewWidth + mViewHeight; // some large distance 3754 for (int index = 0; index < len; index++) { 3755 Event ev = mSelectedEvents.get(index); 3756 float dist = geometry.pointToEvent(x, y, ev); 3757 if (dist < minDist) { 3758 minDist = dist; 3759 closestEvent = ev; 3760 } 3761 } 3762 mSelectedEvent = closestEvent; 3763 3764 // Keep the selected hour and day consistent with the selected 3765 // event. They could be different if we touched on an empty hour 3766 // slot very close to an event in the previous hour slot. In 3767 // that case we will select the nearby event. 3768 int startDay = mSelectedEvent.startDay; 3769 int endDay = mSelectedEvent.endDay; 3770 if (mSelectionDay < startDay) { 3771 mSelectionDay = startDay; 3772 } else if (mSelectionDay > endDay) { 3773 mSelectionDay = endDay; 3774 } 3775 3776 int startHour = mSelectedEvent.startTime / 60; 3777 int endHour; 3778 if (mSelectedEvent.startTime < mSelectedEvent.endTime) { 3779 endHour = (mSelectedEvent.endTime - 1) / 60; 3780 } else { 3781 endHour = mSelectedEvent.endTime / 60; 3782 } 3783 3784 if (mSelectionHour < startHour && mSelectionDay == startDay) { 3785 mSelectionHour = startHour; 3786 } else if (mSelectionHour > endHour && mSelectionDay == endDay) { 3787 mSelectionHour = endHour; 3788 } 3789 } 3790 } 3791 3792 // Encapsulates the code to continue the scrolling after the 3793 // finger is lifted. Instead of stopping the scroll immediately, 3794 // the scroll continues to "free spin" and gradually slows down. 3795 private class ContinueScroll implements Runnable { 3796 public void run() { 3797 mScrolling = mScrolling && mScroller.computeScrollOffset(); 3798 if (!mScrolling) { 3799 resetSelectedHour(); 3800 invalidate(); 3801 return; 3802 } 3803 3804 mViewStartY = mScroller.getCurrY(); 3805 3806 if (mViewStartY < 0) { 3807 mViewStartY = 0; 3808 } else if (mViewStartY > mMaxViewStartY) { 3809 mViewStartY = mMaxViewStartY; 3810 } 3811 3812 computeFirstHour(); 3813 post(this); 3814 invalidate(); 3815 } 3816 } 3817 3818 /** 3819 * Cleanup the pop-up and timers. 3820 */ 3821 public void cleanup() { 3822 // Protect against null-pointer exceptions 3823 if (mPopup != null) { 3824 mPopup.dismiss(); 3825 } 3826 mLastPopupEventID = INVALID_EVENT_ID; 3827 Handler handler = getHandler(); 3828 if (handler != null) { 3829 handler.removeCallbacks(mDismissPopup); 3830 handler.removeCallbacks(mUpdateCurrentTime); 3831 } 3832 3833 Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, 3834 mCellHeight); 3835 3836 // Turn off redraw 3837 mRemeasure = false; 3838 } 3839 3840 /** 3841 * Restart the update timer 3842 */ 3843 public void restartCurrentTimeUpdates() { 3844 post(mUpdateCurrentTime); 3845 } 3846 3847 @Override 3848 protected void onDetachedFromWindow() { 3849 cleanup(); 3850 super.onDetachedFromWindow(); 3851 } 3852 3853 class DismissPopup implements Runnable { 3854 public void run() { 3855 // Protect against null-pointer exceptions 3856 if (mPopup != null) { 3857 mPopup.dismiss(); 3858 } 3859 } 3860 } 3861 3862 class UpdateCurrentTime implements Runnable { 3863 public void run() { 3864 long currentTime = System.currentTimeMillis(); 3865 mCurrentTime.set(currentTime); 3866 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) 3867 postDelayed(mUpdateCurrentTime, 3868 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 3869 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 3870 invalidate(); 3871 } 3872 } 3873 3874 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 3875 @Override 3876 public boolean onSingleTapUp(MotionEvent ev) { 3877 DayView.this.doSingleTapUp(ev); 3878 return true; 3879 } 3880 3881 @Override 3882 public void onLongPress(MotionEvent ev) { 3883 DayView.this.doLongPress(ev); 3884 } 3885 3886 @Override 3887 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 3888 DayView.this.doScroll(e1, e2, distanceX, distanceY); 3889 return true; 3890 } 3891 3892 @Override 3893 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 3894 DayView.this.doFling(e1, e2, velocityX, velocityY); 3895 return true; 3896 } 3897 3898 @Override 3899 public boolean onDown(MotionEvent ev) { 3900 DayView.this.doDown(ev); 3901 return true; 3902 } 3903 } 3904 3905 @Override 3906 public boolean onLongClick(View v) { 3907 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 3908 getSelectedTimeInMillis(), 0, -1, -1, -1); 3909 return true; 3910 } 3911} 3912