1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calendar.agenda; 18 19 import com.android.calendar.CalendarController; 20 import com.android.calendar.R; 21 import com.android.calendar.Utils; 22 import com.android.calendar.CalendarController.EventType; 23 import com.android.calendar.CalendarController.ViewType; 24 import com.android.calendar.StickyHeaderListView; 25 26 import android.content.AsyncQueryHandler; 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.provider.CalendarContract; 34 import android.provider.CalendarContract.Attendees; 35 import android.provider.CalendarContract.Calendars; 36 import android.provider.CalendarContract.Instances; 37 import android.text.format.DateUtils; 38 import android.text.format.Time; 39 import android.util.Log; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.view.View.OnClickListener; 43 import android.view.ViewGroup; 44 import android.widget.BaseAdapter; 45 import android.widget.TextView; 46 47 import java.util.Formatter; 48 import java.util.Iterator; 49 import java.util.LinkedList; 50 import java.util.Locale; 51 import java.util.concurrent.ConcurrentLinkedQueue; 52 53 /* 54 Bugs Bugs Bugs: 55 - At rotation and launch time, the initial position is not set properly. This code is calling 56 listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one. 57 - Scroll using trackball isn't repositioning properly after a new adapter is added. 58 - Track ball clicks at the header/footer doesn't work. 59 - Potential ping pong effect if the prefetch window is big and data is limited 60 - Add index in calendar provider 61 62 ToDo ToDo ToDo: 63 Get design of header and footer from designer 64 65 Make scrolling smoother. 66 Test for correctness 67 Loading speed 68 Check for leaks and excessive allocations 69 */ 70 71 public class AgendaWindowAdapter extends BaseAdapter 72 implements StickyHeaderListView.HeaderIndexer{ 73 74 static final boolean BASICLOG = false; 75 static final boolean DEBUGLOG = false; 76 private static final String TAG = "AgendaWindowAdapter"; 77 78 private static final String AGENDA_SORT_ORDER = 79 CalendarContract.Instances.START_DAY + " ASC, " + 80 CalendarContract.Instances.BEGIN + " ASC, " + 81 CalendarContract.Events.TITLE + " ASC"; 82 83 public static final int INDEX_INSTANCE_ID = 0; 84 public static final int INDEX_TITLE = 1; 85 public static final int INDEX_EVENT_LOCATION = 2; 86 public static final int INDEX_ALL_DAY = 3; 87 public static final int INDEX_HAS_ALARM = 4; 88 public static final int INDEX_COLOR = 5; 89 public static final int INDEX_RRULE = 6; 90 public static final int INDEX_BEGIN = 7; 91 public static final int INDEX_END = 8; 92 public static final int INDEX_EVENT_ID = 9; 93 public static final int INDEX_START_DAY = 10; 94 public static final int INDEX_END_DAY = 11; 95 public static final int INDEX_SELF_ATTENDEE_STATUS = 12; 96 public static final int INDEX_ORGANIZER = 13; 97 public static final int INDEX_OWNER_ACCOUNT = 14; 98 public static final int INDEX_CAN_ORGANIZER_RESPOND= 15; 99 public static final int INDEX_TIME_ZONE = 16; 100 101 private static final String[] PROJECTION = new String[] { 102 Instances._ID, // 0 103 Instances.TITLE, // 1 104 Instances.EVENT_LOCATION, // 2 105 Instances.ALL_DAY, // 3 106 Instances.HAS_ALARM, // 4 107 Instances.CALENDAR_COLOR, // 5 108 Instances.RRULE, // 6 109 Instances.BEGIN, // 7 110 Instances.END, // 8 111 Instances.EVENT_ID, // 9 112 Instances.START_DAY, // 10 Julian start day 113 Instances.END_DAY, // 11 Julian end day 114 Instances.SELF_ATTENDEE_STATUS, // 12 115 Instances.ORGANIZER, // 13 116 Instances.OWNER_ACCOUNT, // 14 117 Instances.CAN_ORGANIZER_RESPOND, // 15 118 Instances.EVENT_TIMEZONE, // 16 119 }; 120 121 // Listview may have a bug where the index/position is not consistent when there's a header. 122 // position == positionInListView - OFF_BY_ONE_BUG 123 // TODO Need to look into this. 124 private static final int OFF_BY_ONE_BUG = 1; 125 private static final int MAX_NUM_OF_ADAPTERS = 5; 126 private static final int IDEAL_NUM_OF_EVENTS = 50; 127 private static final int MIN_QUERY_DURATION = 7; // days 128 private static final int MAX_QUERY_DURATION = 60; // days 129 private static final int PREFETCH_BOUNDARY = 1; 130 131 /** Times to auto-expand/retry query after getting no data */ 132 private static final int RETRIES_ON_NO_DATA = 1; 133 134 private Context mContext; 135 private Resources mResources; 136 private QueryHandler mQueryHandler; 137 private AgendaListView mAgendaListView; 138 139 /** The sum of the rows in all the adapters */ 140 private int mRowCount; 141 142 /** The number of times we have queried and gotten no results back */ 143 private int mEmptyCursorCount; 144 145 /** Cached value of the last used adapter */ 146 private DayAdapterInfo mLastUsedInfo; 147 148 private final LinkedList<DayAdapterInfo> mAdapterInfos = 149 new LinkedList<DayAdapterInfo>(); 150 private final ConcurrentLinkedQueue<QuerySpec> mQueryQueue = 151 new ConcurrentLinkedQueue<QuerySpec>(); 152 private TextView mHeaderView; 153 private TextView mFooterView; 154 private boolean mDoneSettingUpHeaderFooter = false; 155 156 private final boolean mIsTabletConfig; 157 private final int mSkipDateHeader; 158 159 /** 160 * When the user scrolled to the top, a query will be made for older events 161 * and this will be incremented. Don't make more requests if 162 * mOlderRequests > mOlderRequestsProcessed. 163 */ 164 private int mOlderRequests; 165 166 /** Number of "older" query that has been processed. */ 167 private int mOlderRequestsProcessed; 168 169 /** 170 * When the user scrolled to the bottom, a query will be made for newer 171 * events and this will be incremented. Don't make more requests if 172 * mNewerRequests > mNewerRequestsProcessed. 173 */ 174 private int mNewerRequests; 175 176 /** Number of "newer" query that has been processed. */ 177 private int mNewerRequestsProcessed; 178 179 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 180 private Formatter mFormatter; 181 private StringBuilder mStringBuilder; 182 private String mTimeZone; 183 184 // defines if to pop-up the current event when the agenda is first shown 185 private boolean mShowEventOnStart; 186 187 private Runnable mTZUpdater = new Runnable() { 188 @Override 189 public void run() { 190 mTimeZone = Utils.getTimeZone(mContext, this); 191 notifyDataSetChanged(); 192 } 193 }; 194 195 private boolean mShuttingDown; 196 private boolean mHideDeclined; 197 198 /** The current search query, or null if none */ 199 private String mSearchQuery; 200 201 private long mSelectedInstanceId = -1; 202 203 private final int mSelectedItemBackgroundColor; 204 private final int mSelectedItemTextColor; 205 206 // Types of Query 207 private static final int QUERY_TYPE_OLDER = 0; // Query for older events 208 private static final int QUERY_TYPE_NEWER = 1; // Query for newer events 209 private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date 210 211 private static class QuerySpec { 212 long queryStartMillis; 213 Time goToTime; 214 int start; 215 int end; 216 String searchQuery; 217 int queryType; 218 219 public QuerySpec(int queryType) { 220 this.queryType = queryType; 221 } 222 223 @Override 224 public int hashCode() { 225 final int prime = 31; 226 int result = 1; 227 result = prime * result + end; 228 result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32)); 229 result = prime * result + queryType; 230 result = prime * result + start; 231 if (searchQuery != null) { 232 result = prime * result + searchQuery.hashCode(); 233 } 234 if (goToTime != null) { 235 long goToTimeMillis = goToTime.toMillis(false); 236 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32)); 237 } 238 return result; 239 } 240 241 @Override 242 public boolean equals(Object obj) { 243 if (this == obj) return true; 244 if (obj == null) return false; 245 if (getClass() != obj.getClass()) return false; 246 QuerySpec other = (QuerySpec) obj; 247 if (end != other.end || queryStartMillis != other.queryStartMillis 248 || queryType != other.queryType || start != other.start 249 || Utils.equals(searchQuery, other.searchQuery)) { 250 return false; 251 } 252 253 if (goToTime != null) { 254 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) { 255 return false; 256 } 257 } else { 258 if (other.goToTime != null) { 259 return false; 260 } 261 } 262 return true; 263 } 264 } 265 266 static class EventInfo { 267 long begin; 268 long end; 269 long id; 270 int startDay; 271 } 272 273 static class DayAdapterInfo { 274 Cursor cursor; 275 AgendaByDayAdapter dayAdapter; 276 int start; // start day of the cursor's coverage 277 int end; // end day of the cursor's coverage 278 int offset; // offset in position in the list view 279 int size; // dayAdapter.getCount() 280 281 public DayAdapterInfo(Context context) { 282 dayAdapter = new AgendaByDayAdapter(context); 283 } 284 285 @Override 286 public String toString() { 287 // Static class, so the time in this toString will not reflect the 288 // home tz settings. This should only affect debugging. 289 Time time = new Time(); 290 StringBuilder sb = new StringBuilder(); 291 time.setJulianDay(start); 292 time.normalize(false); 293 sb.append("Start:").append(time.toString()); 294 time.setJulianDay(end); 295 time.normalize(false); 296 sb.append(" End:").append(time.toString()); 297 sb.append(" Offset:").append(offset); 298 sb.append(" Size:").append(size); 299 return sb.toString(); 300 } 301 } 302 303 public AgendaWindowAdapter(Context context, 304 AgendaListView agendaListView, boolean showEventOnStart) { 305 mContext = context; 306 mResources = context.getResources(); 307 mSelectedItemBackgroundColor = mResources 308 .getColor(R.color.agenda_selected_background_color); 309 mSelectedItemTextColor = mResources.getColor(R.color.agenda_selected_text_color); 310 mIsTabletConfig = Utils.getConfigBool(mContext, R.bool.tablet_config); 311 mSkipDateHeader = mIsTabletConfig ? 0 : 1; 312 313 mTimeZone = Utils.getTimeZone(context, mTZUpdater); 314 mAgendaListView = agendaListView; 315 mQueryHandler = new QueryHandler(context.getContentResolver()); 316 317 mStringBuilder = new StringBuilder(50); 318 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 319 320 mShowEventOnStart = showEventOnStart; 321 322 mSearchQuery = null; 323 324 LayoutInflater inflater = (LayoutInflater) context 325 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 326 mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 327 mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 328 mHeaderView.setText(R.string.loading); 329 mAgendaListView.addHeaderView(mHeaderView); 330 } 331 332 // Method in Adapter 333 @Override 334 public int getViewTypeCount() { 335 return AgendaByDayAdapter.TYPE_LAST; 336 } 337 338 // Method in BaseAdapter 339 @Override 340 public boolean areAllItemsEnabled() { 341 return false; 342 } 343 344 // Method in Adapter 345 @Override 346 public int getItemViewType(int position) { 347 DayAdapterInfo info = getAdapterInfoByPosition(position); 348 if (info != null) { 349 return info.dayAdapter.getItemViewType(position - info.offset); 350 } else { 351 return -1; 352 } 353 } 354 355 // Method in BaseAdapter 356 @Override 357 public boolean isEnabled(int position) { 358 DayAdapterInfo info = getAdapterInfoByPosition(position); 359 if (info != null) { 360 return info.dayAdapter.isEnabled(position - info.offset); 361 } else { 362 return false; 363 } 364 } 365 366 // Abstract Method in BaseAdapter 367 public int getCount() { 368 return mRowCount; 369 } 370 371 // Abstract Method in BaseAdapter 372 public Object getItem(int position) { 373 DayAdapterInfo info = getAdapterInfoByPosition(position); 374 if (info != null) { 375 return info.dayAdapter.getItem(position - info.offset); 376 } else { 377 return null; 378 } 379 } 380 381 // Method in BaseAdapter 382 @Override 383 public boolean hasStableIds() { 384 return true; 385 } 386 387 // Abstract Method in BaseAdapter 388 public long getItemId(int position) { 389 DayAdapterInfo info = getAdapterInfoByPosition(position); 390 if (info != null) { 391 return ((position - info.offset) << 20) + info.start ; 392 } else { 393 return -1; 394 } 395 } 396 397 // Abstract Method in BaseAdapter 398 public View getView(int position, View convertView, ViewGroup parent) { 399 if (position >= (mRowCount - PREFETCH_BOUNDARY) 400 && mNewerRequests <= mNewerRequestsProcessed) { 401 if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: "); 402 mNewerRequests++; 403 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 404 } 405 406 if (position < PREFETCH_BOUNDARY 407 && mOlderRequests <= mOlderRequestsProcessed) { 408 if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: "); 409 mOlderRequests++; 410 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 411 } 412 413 final View v; 414 DayAdapterInfo info = getAdapterInfoByPosition(position); 415 if (info != null) { 416 int offset = position - info.offset; 417 v = info.dayAdapter.getView(offset, convertView, 418 parent); 419 420 // Turn on the past/present separator if the view is a day header 421 // and it is the first day with events after yesterday. 422 if (info.dayAdapter.isDayHeaderView(offset)) { 423 View simpleDivider = v.findViewById(R.id.top_divider_simple); 424 View pastPresentDivider = v.findViewById(R.id.top_divider_past_present); 425 if (info.dayAdapter.isFirstDayAfterYesterday(offset)) { 426 if (simpleDivider != null && pastPresentDivider != null) { 427 simpleDivider.setVisibility(View.GONE); 428 pastPresentDivider.setVisibility(View.VISIBLE); 429 } 430 } else if (simpleDivider != null && pastPresentDivider != null) { 431 simpleDivider.setVisibility(View.VISIBLE); 432 pastPresentDivider.setVisibility(View.GONE); 433 } 434 } 435 } else { 436 // TODO 437 Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position); 438 TextView tv = new TextView(mContext); 439 tv.setText("Bug! " + position); 440 v = tv; 441 } 442 443 // If this is not a tablet config don't do selection highlighting 444 if (!mIsTabletConfig) { 445 return v; 446 } 447 // Show selected marker if this is item is selected 448 boolean selected = false; 449 Object yy = v.getTag(); 450 if (yy instanceof AgendaAdapter.ViewHolder) { 451 AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) yy; 452 selected = mSelectedInstanceId == vh.instanceId; 453 vh.selectedMarker.setVisibility((selected && mShowEventOnStart) ? 454 View.VISIBLE : View.GONE); 455 if (selected) { 456 mSelectedVH = vh; 457 v.setBackgroundColor(mSelectedItemBackgroundColor); 458 vh.title.setTextColor(mSelectedItemTextColor); 459 vh.when.setTextColor(mSelectedItemTextColor); 460 vh.where.setTextColor(mSelectedItemTextColor); 461 } 462 } 463 464 if (DEBUGLOG) { 465 Log.e(TAG, "getView " + position + " = " + getViewTitle(v)); 466 } 467 return v; 468 } 469 470 private AgendaAdapter.ViewHolder mSelectedVH = null; 471 472 private int findDayPositionNearestTime(Time time) { 473 if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time); 474 475 DayAdapterInfo info = getAdapterInfoByTime(time); 476 if (info != null) { 477 return info.offset + info.dayAdapter.findDayPositionNearestTime(time); 478 } else { 479 return -1; 480 } 481 } 482 483 protected DayAdapterInfo getAdapterInfoByPosition(int position) { 484 synchronized (mAdapterInfos) { 485 if (mLastUsedInfo != null && mLastUsedInfo.offset <= position 486 && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) { 487 return mLastUsedInfo; 488 } 489 for (DayAdapterInfo info : mAdapterInfos) { 490 if (info.offset <= position 491 && position < (info.offset + info.size)) { 492 mLastUsedInfo = info; 493 return info; 494 } 495 } 496 } 497 return null; 498 } 499 500 private DayAdapterInfo getAdapterInfoByTime(Time time) { 501 if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString()); 502 503 Time tmpTime = new Time(time); 504 long timeInMillis = tmpTime.normalize(true); 505 int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff); 506 synchronized (mAdapterInfos) { 507 for (DayAdapterInfo info : mAdapterInfos) { 508 if (info.start <= day && day <= info.end) { 509 return info; 510 } 511 } 512 } 513 return null; 514 } 515 516 public EventInfo getEventByPosition(final int positionInListView) { 517 return getEventByPosition(positionInListView, true); 518 } 519 520 /** 521 * Return the event info for a given position in the adapter 522 * @param positionInListView 523 * @param returnEventStartDay If true, return actual event startday. Otherwise 524 * return agenda date-header date as the startDay. 525 * The two will differ for multi-day events after the first day. 526 * @return 527 */ 528 public EventInfo getEventByPosition(final int positionInListView, 529 boolean returnEventStartDay) { 530 if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + positionInListView); 531 if (positionInListView < 0) { 532 return null; 533 } 534 535 final int positionInAdapter = positionInListView - OFF_BY_ONE_BUG; 536 DayAdapterInfo info = getAdapterInfoByPosition(positionInAdapter); 537 if (info == null) { 538 return null; 539 } 540 541 int cursorPosition = info.dayAdapter.getCursorPosition(positionInAdapter - info.offset); 542 if (cursorPosition == Integer.MIN_VALUE) { 543 return null; 544 } 545 546 boolean isDayHeader = false; 547 if (cursorPosition < 0) { 548 cursorPosition = -cursorPosition; 549 isDayHeader = true; 550 } 551 552 if (cursorPosition < info.cursor.getCount()) { 553 info.cursor.moveToPosition(cursorPosition); 554 EventInfo ei = buildEventInfoFromCursor(info.cursor, isDayHeader); 555 if (!returnEventStartDay && !isDayHeader) { 556 ei.startDay = info.dayAdapter.findJulianDayFromPosition(cursorPosition); 557 } 558 return ei; 559 } 560 return null; 561 } 562 563 private EventInfo buildEventInfoFromCursor(final Cursor cursor, boolean isDayHeader) { 564 EventInfo event = new EventInfo(); 565 event.begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 566 event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END); 567 event.startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 568 569 boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 570 if (allDay) { // UTC 571 Time time = new Time(mTimeZone); 572 time.setJulianDay(Time.getJulianDay(event.begin, 0)); 573 event.begin = time.toMillis(false /* use isDst */); 574 } else if (isDayHeader) { // Trim to midnight. 575 Time time = new Time(mTimeZone); 576 time.set(event.begin); 577 time.hour = 0; 578 time.minute = 0; 579 time.second = 0; 580 event.begin = time.toMillis(false /* use isDst */); 581 } 582 583 if (!isDayHeader) { 584 if (allDay) { 585 Time time = new Time(mTimeZone); 586 time.setJulianDay(Time.getJulianDay(event.end, 0)); 587 event.end = time.toMillis(false /* use isDst */); 588 } else { 589 event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END); 590 } 591 592 event.id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID); 593 } 594 return event; 595 } 596 597 public void refresh(Time goToTime, long id, String searchQuery, boolean forced) { 598 if (searchQuery != null) { 599 mSearchQuery = searchQuery; 600 } 601 602 if (DEBUGLOG) { 603 Log.e(TAG, this + ": refresh " + goToTime.toString() 604 + (forced ? " forced" : " not forced")); 605 } 606 607 int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff); 608 609 if (!forced && isInRange(startDay, startDay)) { 610 // No need to re-query 611 if (!mAgendaListView.isEventVisible(goToTime, id)) { 612 int gotoPosition = findDayPositionNearestTime(goToTime); 613 if (gotoPosition > 0) { 614 mAgendaListView.setSelection(gotoPosition + OFF_BY_ONE_BUG + mSkipDateHeader); 615 } 616 Time actualTime = new Time(mTimeZone); 617 if (goToTime != null) { 618 actualTime.set(goToTime); 619 } else { 620 actualTime.set(mAgendaListView.getFirstVisibleTime()); 621 } 622 CalendarController.getInstance(mContext).sendEvent(this, EventType.UPDATE_TITLE, 623 actualTime, actualTime, -1, ViewType.CURRENT); 624 } 625 return; 626 } 627 628 // Query for a total of MIN_QUERY_DURATION days 629 int endDay = startDay + MIN_QUERY_DURATION; 630 631 queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN); 632 633 // Pre-fetch more data to overcome a race condition in AgendaListView.shiftSelection 634 // Queuing more data with the goToTime set to the selected time skips the call to 635 // shiftSelection on refresh. 636 mOlderRequests++; 637 queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_OLDER); 638 mNewerRequests++; 639 queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_NEWER); 640 641 } 642 643 public void close() { 644 mShuttingDown = true; 645 pruneAdapterInfo(QUERY_TYPE_CLEAN); 646 if (mQueryHandler != null) { 647 mQueryHandler.cancelOperation(0); 648 } 649 } 650 651 private DayAdapterInfo pruneAdapterInfo(int queryType) { 652 synchronized (mAdapterInfos) { 653 DayAdapterInfo recycleMe = null; 654 if (!mAdapterInfos.isEmpty()) { 655 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) { 656 if (queryType == QUERY_TYPE_NEWER) { 657 recycleMe = mAdapterInfos.removeFirst(); 658 } else if (queryType == QUERY_TYPE_OLDER) { 659 recycleMe = mAdapterInfos.removeLast(); 660 // Keep the size only if the oldest items are removed. 661 recycleMe.size = 0; 662 } 663 if (recycleMe != null) { 664 if (recycleMe.cursor != null) { 665 recycleMe.cursor.close(); 666 } 667 return recycleMe; 668 } 669 } 670 671 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) { 672 mRowCount = 0; 673 int deletedRows = 0; 674 DayAdapterInfo info; 675 do { 676 info = mAdapterInfos.poll(); 677 if (info != null) { 678 // TODO the following causes ANR's. Do this in a thread. 679 info.cursor.close(); 680 deletedRows += info.size; 681 recycleMe = info; 682 } 683 } while (info != null); 684 685 if (recycleMe != null) { 686 recycleMe.cursor = null; 687 recycleMe.size = deletedRows; 688 } 689 } 690 } 691 return recycleMe; 692 } 693 } 694 695 private String buildQuerySelection() { 696 // Respect the preference to show/hide declined events 697 698 if (mHideDeclined) { 699 return Calendars.VISIBLE + "=1 AND " 700 + Instances.SELF_ATTENDEE_STATUS + "!=" 701 + Attendees.ATTENDEE_STATUS_DECLINED; 702 } else { 703 return Calendars.VISIBLE + "=1"; 704 } 705 } 706 707 private Uri buildQueryUri(int start, int end, String searchQuery) { 708 Uri rootUri = searchQuery == null ? 709 Instances.CONTENT_BY_DAY_URI : 710 Instances.CONTENT_SEARCH_BY_DAY_URI; 711 Uri.Builder builder = rootUri.buildUpon(); 712 ContentUris.appendId(builder, start); 713 ContentUris.appendId(builder, end); 714 if (searchQuery != null) { 715 builder.appendPath(searchQuery); 716 } 717 return builder.build(); 718 } 719 720 private boolean isInRange(int start, int end) { 721 synchronized (mAdapterInfos) { 722 if (mAdapterInfos.isEmpty()) { 723 return false; 724 } 725 return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end; 726 } 727 } 728 729 private int calculateQueryDuration(int start, int end) { 730 int queryDuration = MAX_QUERY_DURATION; 731 if (mRowCount != 0) { 732 queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount; 733 } 734 735 if (queryDuration > MAX_QUERY_DURATION) { 736 queryDuration = MAX_QUERY_DURATION; 737 } else if (queryDuration < MIN_QUERY_DURATION) { 738 queryDuration = MIN_QUERY_DURATION; 739 } 740 741 return queryDuration; 742 } 743 744 private boolean queueQuery(int start, int end, Time goToTime, 745 String searchQuery, int queryType) { 746 QuerySpec queryData = new QuerySpec(queryType); 747 queryData.goToTime = goToTime; 748 queryData.start = start; 749 queryData.end = end; 750 queryData.searchQuery = searchQuery; 751 return queueQuery(queryData); 752 } 753 754 private boolean queueQuery(QuerySpec queryData) { 755 queryData.searchQuery = mSearchQuery; 756 Boolean queuedQuery; 757 synchronized (mQueryQueue) { 758 queuedQuery = false; 759 Boolean doQueryNow = mQueryQueue.isEmpty(); 760 mQueryQueue.add(queryData); 761 queuedQuery = true; 762 if (doQueryNow) { 763 doQuery(queryData); 764 } 765 } 766 return queuedQuery; 767 } 768 769 private void doQuery(QuerySpec queryData) { 770 if (!mAdapterInfos.isEmpty()) { 771 int start = mAdapterInfos.getFirst().start; 772 int end = mAdapterInfos.getLast().end; 773 int queryDuration = calculateQueryDuration(start, end); 774 switch(queryData.queryType) { 775 case QUERY_TYPE_OLDER: 776 queryData.end = start - 1; 777 queryData.start = queryData.end - queryDuration; 778 break; 779 case QUERY_TYPE_NEWER: 780 queryData.start = end + 1; 781 queryData.end = queryData.start + queryDuration; 782 break; 783 } 784 785 // By "compacting" cursors, this fixes the disco/ping-pong problem 786 // b/5311977 787 if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) { 788 if (DEBUGLOG) { 789 Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount 790 + " totalStart:" + start 791 + " totalEnd:" + end 792 + " query.start:" + queryData.start 793 + " query.end:" + queryData.end); 794 } 795 796 queryData.queryType = QUERY_TYPE_CLEAN; 797 798 if (queryData.start > start) { 799 queryData.start = start; 800 } 801 if (queryData.end < end) { 802 queryData.end = end; 803 } 804 } 805 } 806 807 if (BASICLOG) { 808 Time time = new Time(mTimeZone); 809 time.setJulianDay(queryData.start); 810 Time time2 = new Time(mTimeZone); 811 time2.setJulianDay(queryData.end); 812 Log.v(TAG, "startQuery: " + time.toString() + " to " 813 + time2.toString() + " then go to " + queryData.goToTime); 814 } 815 816 mQueryHandler.cancelOperation(0); 817 if (BASICLOG) queryData.queryStartMillis = System.nanoTime(); 818 819 Uri queryUri = buildQueryUri( 820 queryData.start, queryData.end, queryData.searchQuery); 821 mQueryHandler.startQuery(0, queryData, queryUri, 822 PROJECTION, buildQuerySelection(), null, 823 AGENDA_SORT_ORDER); 824 } 825 826 private String formatDateString(int julianDay) { 827 Time time = new Time(mTimeZone); 828 time.setJulianDay(julianDay); 829 long millis = time.toMillis(false); 830 mStringBuilder.setLength(0); 831 return DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 832 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE 833 | DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString(); 834 } 835 836 private void updateHeaderFooter(final int start, final int end) { 837 mHeaderView.setText(mContext.getString(R.string.show_older_events, 838 formatDateString(start))); 839 mFooterView.setText(mContext.getString(R.string.show_newer_events, 840 formatDateString(end))); 841 } 842 843 private class QueryHandler extends AsyncQueryHandler { 844 845 public QueryHandler(ContentResolver cr) { 846 super(cr); 847 } 848 849 @Override 850 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 851 QuerySpec data = (QuerySpec)cookie; 852 if (BASICLOG) { 853 long queryEndMillis = System.nanoTime(); 854 Log.e(TAG, "Query time(ms): " 855 + (queryEndMillis - data.queryStartMillis) / 1000000 856 + " Count: " + cursor.getCount()); 857 } 858 859 if (mShuttingDown) { 860 cursor.close(); 861 return; 862 } 863 864 // Notify Listview of changes and update position 865 int cursorSize = cursor.getCount(); 866 if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) { 867 final int listPositionOffset = processNewCursor(data, cursor); 868 if (data.goToTime == null) { // Typical Scrolling type query 869 notifyDataSetChanged(); 870 if (listPositionOffset != 0) { 871 mAgendaListView.shiftSelection(listPositionOffset); 872 } 873 } else { // refresh() called. Go to the designated position 874 final Time goToTime = data.goToTime; 875 notifyDataSetChanged(); 876 int newPosition = findDayPositionNearestTime(goToTime); 877 if (newPosition >= 0) { 878 mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG 879 + mSkipDateHeader); 880 Time actualTime = new Time(mTimeZone); 881 actualTime.set(goToTime); 882 CalendarController.getInstance(mContext).sendEvent(this, 883 EventType.UPDATE_TITLE, actualTime, actualTime, -1, 884 ViewType.CURRENT); 885 } 886 if (DEBUGLOG) { 887 Log.e(TAG, "Setting listview to " + 888 "findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG)); 889 } 890 } 891 892 // size == 1 means a fresh query. Possibly after the data changed. 893 // Let's check whether mSelectedInstanceId is still valid. 894 if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) { 895 boolean found = false; 896 cursor.moveToPosition(-1); 897 while (cursor.moveToNext()) { 898 if (mSelectedInstanceId == cursor 899 .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) { 900 found = true; 901 break; 902 } 903 }; 904 905 if (!found) { 906 mSelectedInstanceId = -1; 907 } 908 } 909 910 if (mSelectedInstanceId == -1 && cursor.moveToFirst()) { 911 mSelectedInstanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID); 912 // Set up a dummy view holder so we have the right all day 913 // info when the view is created. 914 // TODO determine the full set of what might be useful to 915 // know about the selected view and fill it in. 916 mSelectedVH = new AgendaAdapter.ViewHolder(); 917 mSelectedVH.allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 918 919 EventInfo event = buildEventInfoFromCursor(cursor, false); 920 if (mShowEventOnStart) { 921 CalendarController.getInstance(mContext).sendEventRelatedEvent(this, 922 EventType.VIEW_EVENT, event.id, event.begin, event.end, 0, 0, -1); 923 } 924 } 925 } else { 926 cursor.close(); 927 } 928 929 // Update header and footer 930 if (!mDoneSettingUpHeaderFooter) { 931 OnClickListener headerFooterOnClickListener = new OnClickListener() { 932 public void onClick(View v) { 933 if (v == mHeaderView) { 934 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 935 } else { 936 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 937 } 938 }}; 939 mHeaderView.setOnClickListener(headerFooterOnClickListener); 940 mFooterView.setOnClickListener(headerFooterOnClickListener); 941 mAgendaListView.addFooterView(mFooterView); 942 mDoneSettingUpHeaderFooter = true; 943 } 944 synchronized (mQueryQueue) { 945 int totalAgendaRangeStart = -1; 946 int totalAgendaRangeEnd = -1; 947 948 if (cursorSize != 0) { 949 // Remove the query that just completed 950 QuerySpec x = mQueryQueue.poll(); 951 if (BASICLOG && !x.equals(data)) { 952 Log.e(TAG, "onQueryComplete - cookie != head of queue"); 953 } 954 mEmptyCursorCount = 0; 955 if (data.queryType == QUERY_TYPE_NEWER) { 956 mNewerRequestsProcessed++; 957 } else if (data.queryType == QUERY_TYPE_OLDER) { 958 mOlderRequestsProcessed++; 959 } 960 961 totalAgendaRangeStart = mAdapterInfos.getFirst().start; 962 totalAgendaRangeEnd = mAdapterInfos.getLast().end; 963 } else { // CursorSize == 0 964 QuerySpec querySpec = mQueryQueue.peek(); 965 966 // Update Adapter Info with new start and end date range 967 if (!mAdapterInfos.isEmpty()) { 968 DayAdapterInfo first = mAdapterInfos.getFirst(); 969 DayAdapterInfo last = mAdapterInfos.getLast(); 970 971 if (first.start - 1 <= querySpec.end && querySpec.start < first.start) { 972 first.start = querySpec.start; 973 } 974 975 if (querySpec.start <= last.end + 1 && last.end < querySpec.end) { 976 last.end = querySpec.end; 977 } 978 979 totalAgendaRangeStart = first.start; 980 totalAgendaRangeEnd = last.end; 981 } else { 982 totalAgendaRangeStart = querySpec.start; 983 totalAgendaRangeEnd = querySpec.end; 984 } 985 986 // Update query specification with expanded search range 987 // and maybe rerun query 988 switch (querySpec.queryType) { 989 case QUERY_TYPE_OLDER: 990 totalAgendaRangeStart = querySpec.start; 991 querySpec.start -= MAX_QUERY_DURATION; 992 break; 993 case QUERY_TYPE_NEWER: 994 totalAgendaRangeEnd = querySpec.end; 995 querySpec.end += MAX_QUERY_DURATION; 996 break; 997 case QUERY_TYPE_CLEAN: 998 totalAgendaRangeStart = querySpec.start; 999 totalAgendaRangeEnd = querySpec.end; 1000 querySpec.start -= MAX_QUERY_DURATION / 2; 1001 querySpec.end += MAX_QUERY_DURATION / 2; 1002 break; 1003 } 1004 1005 if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) { 1006 // Nothing in the cursor again. Dropping query 1007 mQueryQueue.poll(); 1008 } 1009 } 1010 1011 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd); 1012 1013 // Go over the events and mark the first day after yesterday 1014 // that has events in it 1015 synchronized (mAdapterInfos) { 1016 DayAdapterInfo info = mAdapterInfos.getFirst(); 1017 if (info != null) { 1018 Time time = new Time(mTimeZone); 1019 long now = System.currentTimeMillis(); 1020 time.set(now); 1021 int JulianToday = Time.getJulianDay(now, time.gmtoff); 1022 Iterator<DayAdapterInfo> iter = mAdapterInfos.iterator(); 1023 boolean foundDay = false; 1024 while (iter.hasNext() && !foundDay) { 1025 info = iter.next(); 1026 for (int i = 0; i < info.size; i++) { 1027 if (info.dayAdapter.findJulianDayFromPosition(i) >= JulianToday) { 1028 info.dayAdapter.setAsFirstDayAfterYesterday(i); 1029 foundDay = true; 1030 break; 1031 } 1032 } 1033 } 1034 } 1035 } 1036 1037 // Fire off the next query if any 1038 Iterator<QuerySpec> it = mQueryQueue.iterator(); 1039 while (it.hasNext()) { 1040 QuerySpec queryData = it.next(); 1041 if (!isInRange(queryData.start, queryData.end)) { 1042 // Query accepted 1043 if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size()); 1044 doQuery(queryData); 1045 break; 1046 } else { 1047 // Query rejected 1048 it.remove(); 1049 if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size()); 1050 } 1051 } 1052 } 1053 if (BASICLOG) { 1054 for (DayAdapterInfo info3 : mAdapterInfos) { 1055 Log.e(TAG, "> " + info3.toString()); 1056 } 1057 } 1058 } 1059 1060 /* 1061 * Update the adapter info array with a the new cursor. Close out old 1062 * cursors as needed. 1063 * 1064 * @return number of rows removed from the beginning 1065 */ 1066 private int processNewCursor(QuerySpec data, Cursor cursor) { 1067 synchronized (mAdapterInfos) { 1068 // Remove adapter info's from adapterInfos as needed 1069 DayAdapterInfo info = pruneAdapterInfo(data.queryType); 1070 int listPositionOffset = 0; 1071 if (info == null) { 1072 info = new DayAdapterInfo(mContext); 1073 } else { 1074 if (DEBUGLOG) 1075 Log.e(TAG, "processNewCursor listPositionOffsetA=" 1076 + -info.size); 1077 listPositionOffset = -info.size; 1078 } 1079 1080 // Setup adapter info 1081 info.start = data.start; 1082 info.end = data.end; 1083 info.cursor = cursor; 1084 info.dayAdapter.changeCursor(info); 1085 info.size = info.dayAdapter.getCount(); 1086 1087 // Insert into adapterInfos 1088 if (mAdapterInfos.isEmpty() 1089 || data.end <= mAdapterInfos.getFirst().start) { 1090 mAdapterInfos.addFirst(info); 1091 listPositionOffset += info.size; 1092 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) { 1093 mAdapterInfos.addLast(info); 1094 for (DayAdapterInfo info2 : mAdapterInfos) { 1095 Log.e("========== BUG ==", info2.toString()); 1096 } 1097 } else { 1098 mAdapterInfos.addLast(info); 1099 } 1100 1101 // Update offsets in adapterInfos 1102 mRowCount = 0; 1103 for (DayAdapterInfo info3 : mAdapterInfos) { 1104 info3.offset = mRowCount; 1105 mRowCount += info3.size; 1106 } 1107 mLastUsedInfo = null; 1108 1109 return listPositionOffset; 1110 } 1111 } 1112 } 1113 1114 static String getViewTitle(View x) { 1115 String title = ""; 1116 if (x != null) { 1117 Object yy = x.getTag(); 1118 if (yy instanceof AgendaAdapter.ViewHolder) { 1119 TextView tv = ((AgendaAdapter.ViewHolder) yy).title; 1120 if (tv != null) { 1121 title = (String) tv.getText(); 1122 } 1123 } else if (yy != null) { 1124 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView; 1125 if (dateView != null) { 1126 title = (String) dateView.getText(); 1127 } 1128 } 1129 } 1130 return title; 1131 } 1132 1133 public void onResume() { 1134 mTZUpdater.run(); 1135 } 1136 1137 public void setHideDeclinedEvents(boolean hideDeclined) { 1138 mHideDeclined = hideDeclined; 1139 } 1140 1141 public void setSelectedView(View v) { 1142 if (v != null) { 1143 Object vh = v.getTag(); 1144 if (vh instanceof AgendaAdapter.ViewHolder) { 1145 mSelectedVH = (AgendaAdapter.ViewHolder) vh; 1146 mSelectedInstanceId = mSelectedVH.instanceId; 1147 } 1148 } 1149 } 1150 1151 public AgendaAdapter.ViewHolder getSelectedViewHolder() { 1152 return mSelectedVH; 1153 } 1154 1155 public long getSelectedInstanceId() { 1156 return mSelectedInstanceId; 1157 } 1158 1159 public void setSelectedInstanceId(long selectedInstanceId) { 1160 mSelectedInstanceId = selectedInstanceId; 1161 mSelectedVH = null; 1162 } 1163 1164 1165 // Implementation of HeaderIndexer interface for StickyHeeaderListView 1166 1167 // Returns the location of the day header of a specific event specified in the position 1168 // in the adapter 1169 public int getHeaderPositionFromItemPosition(int position) { 1170 1171 // For phone configuration, return -1 so there will be no sticky header 1172 if (!mIsTabletConfig) { 1173 return -1; 1174 } 1175 1176 DayAdapterInfo info = getAdapterInfoByPosition(position); 1177 if (info != null) { 1178 int pos = info.dayAdapter.getHeaderPosition(position - info.offset); 1179 return (pos != -1)?(pos + info.offset):-1; 1180 } 1181 return -1; 1182 } 1183 1184 // Returns the number of events for a specific day header 1185 public int getHeaderItemsNumber(int headerPosition) { 1186 if (headerPosition < 0 || !mIsTabletConfig) { 1187 return -1; 1188 } 1189 DayAdapterInfo info = getAdapterInfoByPosition(headerPosition); 1190 if (info != null) { 1191 return info.dayAdapter.getHeaderItemsCount(headerPosition - info.offset); 1192 } 1193 return -1; 1194 } 1195 } 1196