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; 18 19 import android.content.AsyncQueryHandler; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.provider.Calendar.Attendees; 25 import android.provider.Calendar.Calendars; 26 import android.provider.Calendar.Instances; 27 import android.text.format.DateUtils; 28 import android.text.format.Time; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.View.OnClickListener; 34 import android.widget.BaseAdapter; 35 import android.widget.TextView; 36 37 import java.util.Formatter; 38 import java.util.Iterator; 39 import java.util.LinkedList; 40 import java.util.Locale; 41 import java.util.concurrent.ConcurrentLinkedQueue; 42 43 /* 44 Bugs Bugs Bugs: 45 - At rotation and launch time, the initial position is not set properly. This code is calling 46 listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one. 47 - Scroll using trackball isn't repositioning properly after a new adapter is added. 48 - Track ball clicks at the header/footer doesn't work. 49 - Potential ping pong effect if the prefetch window is big and data is limited 50 - Add index in calendar provider 51 52 ToDo ToDo ToDo: 53 Get design of header and footer from designer 54 55 Make scrolling smoother. 56 Test for correctness 57 Loading speed 58 Check for leaks and excessive allocations 59 */ 60 61 public class AgendaWindowAdapter extends BaseAdapter { 62 63 static final boolean BASICLOG = false; 64 static final boolean DEBUGLOG = false; 65 private static String TAG = "AgendaWindowAdapter"; 66 67 private static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC"; 68 public static final int INDEX_TITLE = 1; 69 public static final int INDEX_EVENT_LOCATION = 2; 70 public static final int INDEX_ALL_DAY = 3; 71 public static final int INDEX_HAS_ALARM = 4; 72 public static final int INDEX_COLOR = 5; 73 public static final int INDEX_RRULE = 6; 74 public static final int INDEX_BEGIN = 7; 75 public static final int INDEX_END = 8; 76 public static final int INDEX_EVENT_ID = 9; 77 public static final int INDEX_START_DAY = 10; 78 public static final int INDEX_END_DAY = 11; 79 public static final int INDEX_SELF_ATTENDEE_STATUS = 12; 80 81 private static final String[] PROJECTION = new String[] { 82 Instances._ID, // 0 83 Instances.TITLE, // 1 84 Instances.EVENT_LOCATION, // 2 85 Instances.ALL_DAY, // 3 86 Instances.HAS_ALARM, // 4 87 Instances.COLOR, // 5 88 Instances.RRULE, // 6 89 Instances.BEGIN, // 7 90 Instances.END, // 8 91 Instances.EVENT_ID, // 9 92 Instances.START_DAY, // 10 Julian start day 93 Instances.END_DAY, // 11 Julian end day 94 Instances.SELF_ATTENDEE_STATUS, // 12 95 }; 96 97 // Listview may have a bug where the index/position is not consistent when there's a header. 98 // TODO Need to look into this. 99 private static final int OFF_BY_ONE_BUG = 1; 100 101 private static final int MAX_NUM_OF_ADAPTERS = 5; 102 103 private static final int IDEAL_NUM_OF_EVENTS = 50; 104 105 private static final int MIN_QUERY_DURATION = 7; // days 106 107 private static final int MAX_QUERY_DURATION = 60; // days 108 109 private static final int PREFETCH_BOUNDARY = 1; 110 111 // Times to auto-expand/retry query after getting no data 112 private static final int RETRIES_ON_NO_DATA = 0; 113 114 private Context mContext; 115 116 private QueryHandler mQueryHandler; 117 118 private AgendaListView mAgendaListView; 119 120 private int mRowCount; // The sum of the rows in all the adapters 121 122 private int mEmptyCursorCount; 123 124 private DayAdapterInfo mLastUsedInfo; // Cached value of the last used adapter. 125 126 private LinkedList<DayAdapterInfo> mAdapterInfos = new LinkedList<DayAdapterInfo>(); 127 128 private ConcurrentLinkedQueue<QuerySpec> mQueryQueue = new ConcurrentLinkedQueue<QuerySpec>(); 129 130 private TextView mHeaderView; 131 132 private TextView mFooterView; 133 134 private boolean mDoneSettingUpHeaderFooter = false; 135 136 /* 137 * When the user scrolled to the top, a query will be made for older events 138 * and this will be incremented. Don't make more requests if 139 * mOlderRequests > mOlderRequestsProcessed. 140 */ 141 private int mOlderRequests; 142 143 // Number of "older" query that has been processed. 144 private int mOlderRequestsProcessed; 145 146 /* 147 * When the user scrolled to the bottom, a query will be made for newer 148 * events and this will be incremented. Don't make more requests if 149 * mNewerRequests > mNewerRequestsProcessed. 150 */ 151 private int mNewerRequests; 152 153 // Number of "newer" query that has been processed. 154 private int mNewerRequestsProcessed; 155 156 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 157 private Formatter mFormatter; 158 private StringBuilder mStringBuilder; 159 160 private boolean mShuttingDown; 161 private boolean mHideDeclined; 162 163 // Types of Query 164 private static final int QUERY_TYPE_OLDER = 0; // Query for older events 165 private static final int QUERY_TYPE_NEWER = 1; // Query for newer events 166 private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date 167 168 private static class QuerySpec { 169 long queryStartMillis; 170 171 Time goToTime; 172 173 int start; 174 175 int end; 176 177 int queryType; 178 179 public QuerySpec(int queryType) { 180 this.queryType = queryType; 181 } 182 183 @Override 184 public int hashCode() { 185 final int prime = 31; 186 int result = 1; 187 result = prime * result + end; 188 result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32)); 189 result = prime * result + queryType; 190 result = prime * result + start; 191 if (goToTime != null) { 192 long goToTimeMillis = goToTime.toMillis(false); 193 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32)); 194 } 195 return result; 196 } 197 198 @Override 199 public boolean equals(Object obj) { 200 if (this == obj) return true; 201 if (obj == null) return false; 202 if (getClass() != obj.getClass()) return false; 203 QuerySpec other = (QuerySpec) obj; 204 if (end != other.end || queryStartMillis != other.queryStartMillis 205 || queryType != other.queryType || start != other.start) { 206 return false; 207 } 208 if (goToTime != null) { 209 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) { 210 return false; 211 } 212 } else { 213 if (other.goToTime != null) { 214 return false; 215 } 216 } 217 return true; 218 } 219 } 220 221 static class EventInfo { 222 long begin; 223 224 long end; 225 226 long id; 227 } 228 229 static class DayAdapterInfo { 230 Cursor cursor; 231 232 AgendaByDayAdapter dayAdapter; 233 234 int start; // start day of the cursor's coverage 235 236 int end; // end day of the cursor's coverage 237 238 int offset; // offset in position in the list view 239 240 int size; // dayAdapter.getCount() 241 242 public DayAdapterInfo(Context context) { 243 dayAdapter = new AgendaByDayAdapter(context); 244 } 245 246 @Override 247 public String toString() { 248 Time time = new Time(); 249 StringBuilder sb = new StringBuilder(); 250 time.setJulianDay(start); 251 time.normalize(false); 252 sb.append("Start:").append(time.toString()); 253 time.setJulianDay(end); 254 time.normalize(false); 255 sb.append(" End:").append(time.toString()); 256 sb.append(" Offset:").append(offset); 257 sb.append(" Size:").append(size); 258 return sb.toString(); 259 } 260 } 261 262 public AgendaWindowAdapter(AgendaActivity agendaActivity, 263 AgendaListView agendaListView) { 264 mContext = agendaActivity; 265 mAgendaListView = agendaListView; 266 mQueryHandler = new QueryHandler(agendaActivity.getContentResolver()); 267 268 mStringBuilder = new StringBuilder(50); 269 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 270 271 LayoutInflater inflater = (LayoutInflater) agendaActivity 272 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 273 mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 274 mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 275 mHeaderView.setText(R.string.loading); 276 mAgendaListView.addHeaderView(mHeaderView); 277 } 278 279 // Method in Adapter 280 @Override 281 public int getViewTypeCount() { 282 return AgendaByDayAdapter.TYPE_LAST; 283 } 284 285 // Method in BaseAdapter 286 @Override 287 public boolean areAllItemsEnabled() { 288 return false; 289 } 290 291 // Method in Adapter 292 @Override 293 public int getItemViewType(int position) { 294 DayAdapterInfo info = getAdapterInfoByPosition(position); 295 if (info != null) { 296 return info.dayAdapter.getItemViewType(position - info.offset); 297 } else { 298 return -1; 299 } 300 } 301 302 // Method in BaseAdapter 303 @Override 304 public boolean isEnabled(int position) { 305 DayAdapterInfo info = getAdapterInfoByPosition(position); 306 if (info != null) { 307 return info.dayAdapter.isEnabled(position - info.offset); 308 } else { 309 return false; 310 } 311 } 312 313 // Abstract Method in BaseAdapter 314 public int getCount() { 315 return mRowCount; 316 } 317 318 // Abstract Method in BaseAdapter 319 public Object getItem(int position) { 320 DayAdapterInfo info = getAdapterInfoByPosition(position); 321 if (info != null) { 322 return info.dayAdapter.getItem(position - info.offset); 323 } else { 324 return null; 325 } 326 } 327 328 // Method in BaseAdapter 329 @Override 330 public boolean hasStableIds() { 331 return true; 332 } 333 334 // Abstract Method in BaseAdapter 335 public long getItemId(int position) { 336 DayAdapterInfo info = getAdapterInfoByPosition(position); 337 if (info != null) { 338 return ((position - info.offset) << 20) + info.start ; 339 } else { 340 return -1; 341 } 342 } 343 344 // Abstract Method in BaseAdapter 345 public View getView(int position, View convertView, ViewGroup parent) { 346 if (position >= (mRowCount - PREFETCH_BOUNDARY) 347 && mNewerRequests <= mNewerRequestsProcessed) { 348 if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: "); 349 mNewerRequests++; 350 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 351 } 352 353 if (position < PREFETCH_BOUNDARY 354 && mOlderRequests <= mOlderRequestsProcessed) { 355 if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: "); 356 mOlderRequests++; 357 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 358 } 359 360 View v; 361 DayAdapterInfo info = getAdapterInfoByPosition(position); 362 if (info != null) { 363 v = info.dayAdapter.getView(position - info.offset, convertView, 364 parent); 365 } else { 366 //TODO 367 Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position); 368 TextView tv = new TextView(mContext); 369 tv.setText("Bug! " + position); 370 v = tv; 371 } 372 373 if (DEBUGLOG) { 374 Log.e(TAG, "getView " + position + " = " + getViewTitle(v)); 375 } 376 return v; 377 } 378 379 private int findDayPositionNearestTime(Time time) { 380 if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time); 381 382 DayAdapterInfo info = getAdapterInfoByTime(time); 383 if (info != null) { 384 return info.offset + info.dayAdapter.findDayPositionNearestTime(time); 385 } else { 386 return -1; 387 } 388 } 389 390 private DayAdapterInfo getAdapterInfoByPosition(int position) { 391 synchronized (mAdapterInfos) { 392 if (mLastUsedInfo != null && mLastUsedInfo.offset <= position 393 && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) { 394 return mLastUsedInfo; 395 } 396 for (DayAdapterInfo info : mAdapterInfos) { 397 if (info.offset <= position 398 && position < (info.offset + info.size)) { 399 mLastUsedInfo = info; 400 return info; 401 } 402 } 403 } 404 return null; 405 } 406 407 private DayAdapterInfo getAdapterInfoByTime(Time time) { 408 if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString()); 409 410 Time tmpTime = new Time(time); 411 long timeInMillis = tmpTime.normalize(true); 412 int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff); 413 synchronized (mAdapterInfos) { 414 for (DayAdapterInfo info : mAdapterInfos) { 415 if (info.start <= day && day < info.end) { 416 return info; 417 } 418 } 419 } 420 return null; 421 } 422 423 public EventInfo getEventByPosition(int position) { 424 if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + position); 425 426 EventInfo event = new EventInfo(); 427 position -= OFF_BY_ONE_BUG; 428 DayAdapterInfo info = getAdapterInfoByPosition(position); 429 if (info == null) { 430 return null; 431 } 432 433 position = info.dayAdapter.getCursorPosition(position - info.offset); 434 if (position == Integer.MIN_VALUE) { 435 return null; 436 } 437 438 boolean isDayHeader = false; 439 if (position < 0) { 440 position = -position; 441 isDayHeader = true; 442 } 443 444 if (position < info.cursor.getCount()) { 445 info.cursor.moveToPosition(position); 446 event.begin = info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 447 boolean allDay = info.cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 448 449 if (allDay) { // UTC 450 Time time = new Time(); 451 time.setJulianDay(Time.getJulianDay(event.begin, 0)); 452 event.begin = time.toMillis(false /* use isDst */); 453 } else if (isDayHeader) { // Trim to midnight. 454 Time time = new Time(); 455 time.set(event.begin); 456 time.hour = 0; 457 time.minute = 0; 458 time.second = 0; 459 event.begin = time.toMillis(false /* use isDst */); 460 } 461 462 if (!isDayHeader) { 463 event.end = info.cursor.getLong(AgendaWindowAdapter.INDEX_END); 464 event.id = info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID); 465 } 466 return event; 467 } 468 return null; 469 } 470 471 public void refresh(Time goToTime, boolean forced) { 472 if (DEBUGLOG) { 473 Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced")); 474 } 475 476 int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff); 477 478 if (!forced && isInRange(startDay, startDay)) { 479 // No need to requery 480 mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG); 481 return; 482 } 483 484 // Query for a total of MIN_QUERY_DURATION days 485 int endDay = startDay + MIN_QUERY_DURATION; 486 487 queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN); 488 } 489 490 public void close() { 491 mShuttingDown = true; 492 pruneAdapterInfo(QUERY_TYPE_CLEAN); 493 if (mQueryHandler != null) { 494 mQueryHandler.cancelOperation(0); 495 } 496 } 497 498 private DayAdapterInfo pruneAdapterInfo(int queryType) { 499 synchronized (mAdapterInfos) { 500 DayAdapterInfo recycleMe = null; 501 if (!mAdapterInfos.isEmpty()) { 502 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) { 503 if (queryType == QUERY_TYPE_NEWER) { 504 recycleMe = mAdapterInfos.removeFirst(); 505 } else if (queryType == QUERY_TYPE_OLDER) { 506 recycleMe = mAdapterInfos.removeLast(); 507 // Keep the size only if the oldest items are removed. 508 recycleMe.size = 0; 509 } 510 if (recycleMe != null) { 511 if (recycleMe.cursor != null) { 512 recycleMe.cursor.close(); 513 } 514 return recycleMe; 515 } 516 } 517 518 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) { 519 mRowCount = 0; 520 int deletedRows = 0; 521 DayAdapterInfo info; 522 do { 523 info = mAdapterInfos.poll(); 524 if (info != null) { 525 info.cursor.close(); 526 deletedRows += info.size; 527 recycleMe = info; 528 } 529 } while (info != null); 530 531 if (recycleMe != null) { 532 recycleMe.cursor = null; 533 recycleMe.size = deletedRows; 534 } 535 } 536 } 537 return recycleMe; 538 } 539 } 540 541 private String buildQuerySelection() { 542 // Respect the preference to show/hide declined events 543 544 if (mHideDeclined) { 545 return Calendars.SELECTED + "=1 AND " 546 + Instances.SELF_ATTENDEE_STATUS + "!=" 547 + Attendees.ATTENDEE_STATUS_DECLINED; 548 } else { 549 return Calendars.SELECTED + "=1"; 550 } 551 } 552 553 private Uri buildQueryUri(int start, int end) { 554 StringBuilder path = new StringBuilder(); 555 path.append(start); 556 path.append('/'); 557 path.append(end); 558 Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString()); 559 return uri; 560 } 561 562 private boolean isInRange(int start, int end) { 563 synchronized (mAdapterInfos) { 564 if (mAdapterInfos.isEmpty()) { 565 return false; 566 } 567 return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end; 568 } 569 } 570 571 private int calculateQueryDuration(int start, int end) { 572 int queryDuration = MAX_QUERY_DURATION; 573 if (mRowCount != 0) { 574 queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount; 575 } 576 577 if (queryDuration > MAX_QUERY_DURATION) { 578 queryDuration = MAX_QUERY_DURATION; 579 } else if (queryDuration < MIN_QUERY_DURATION) { 580 queryDuration = MIN_QUERY_DURATION; 581 } 582 583 return queryDuration; 584 } 585 586 private boolean queueQuery(int start, int end, Time goToTime, int queryType) { 587 QuerySpec queryData = new QuerySpec(queryType); 588 queryData.goToTime = goToTime; 589 queryData.start = start; 590 queryData.end = end; 591 return queueQuery(queryData); 592 } 593 594 private boolean queueQuery(QuerySpec queryData) { 595 Boolean queuedQuery; 596 synchronized (mQueryQueue) { 597 queuedQuery = false; 598 Boolean doQueryNow = mQueryQueue.isEmpty(); 599 mQueryQueue.add(queryData); 600 queuedQuery = true; 601 if (doQueryNow) { 602 doQuery(queryData); 603 } 604 } 605 return queuedQuery; 606 } 607 608 private void doQuery(QuerySpec queryData) { 609 if (!mAdapterInfos.isEmpty()) { 610 int start = mAdapterInfos.getFirst().start; 611 int end = mAdapterInfos.getLast().end; 612 int queryDuration = calculateQueryDuration(start, end); 613 switch(queryData.queryType) { 614 case QUERY_TYPE_OLDER: 615 queryData.end = start - 1; 616 queryData.start = queryData.end - queryDuration; 617 break; 618 case QUERY_TYPE_NEWER: 619 queryData.start = end + 1; 620 queryData.end = queryData.start + queryDuration; 621 break; 622 } 623 } 624 625 if (BASICLOG) { 626 Time time = new Time(); 627 time.setJulianDay(queryData.start); 628 Time time2 = new Time(); 629 time2.setJulianDay(queryData.end); 630 Log.v(TAG, "startQuery: " + time.toString() + " to " 631 + time2.toString() + " then go to " + queryData.goToTime); 632 } 633 634 mQueryHandler.cancelOperation(0); 635 if (BASICLOG) queryData.queryStartMillis = System.nanoTime(); 636 mQueryHandler.startQuery(0, queryData, buildQueryUri( 637 queryData.start, queryData.end), PROJECTION, 638 buildQuerySelection(), null, AGENDA_SORT_ORDER); 639 } 640 641 private String formatDateString(int julianDay) { 642 Time time = new Time(); 643 time.setJulianDay(julianDay); 644 long millis = time.toMillis(false); 645 mStringBuilder.setLength(0); 646 return DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 647 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE 648 | DateUtils.FORMAT_ABBREV_MONTH).toString(); 649 } 650 651 private void updateHeaderFooter(final int start, final int end) { 652 mHeaderView.setText(mContext.getString(R.string.show_older_events, 653 formatDateString(start))); 654 mFooterView.setText(mContext.getString(R.string.show_newer_events, 655 formatDateString(end))); 656 } 657 658 private class QueryHandler extends AsyncQueryHandler { 659 660 public QueryHandler(ContentResolver cr) { 661 super(cr); 662 } 663 664 @Override 665 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 666 QuerySpec data = (QuerySpec)cookie; 667 if (BASICLOG) { 668 long queryEndMillis = System.nanoTime(); 669 Log.e(TAG, "Query time(ms): " 670 + (queryEndMillis - data.queryStartMillis) / 1000000 671 + " Count: " + cursor.getCount()); 672 } 673 674 if (mShuttingDown) { 675 cursor.close(); 676 return; 677 } 678 679 // Notify Listview of changes and update position 680 int cursorSize = cursor.getCount(); 681 if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) { 682 final int listPositionOffset = processNewCursor(data, cursor); 683 if (data.goToTime == null) { // Typical Scrolling type query 684 notifyDataSetChanged(); 685 if (listPositionOffset != 0) { 686 mAgendaListView.shiftSelection(listPositionOffset); 687 } 688 } else { // refresh() called. Go to the designated position 689 final Time goToTime = data.goToTime; 690 notifyDataSetChanged(); 691 int newPosition = findDayPositionNearestTime(goToTime); 692 if (newPosition >= 0) { 693 mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG); 694 } 695 if (DEBUGLOG) { 696 Log.e(TAG, "Setting listview to " + 697 "findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG)); 698 } 699 } 700 } else { 701 cursor.close(); 702 } 703 704 // Update header and footer 705 if (!mDoneSettingUpHeaderFooter) { 706 OnClickListener headerFooterOnClickListener = new OnClickListener() { 707 public void onClick(View v) { 708 if (v == mHeaderView) { 709 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 710 } else { 711 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 712 } 713 }}; 714 mHeaderView.setOnClickListener(headerFooterOnClickListener); 715 mFooterView.setOnClickListener(headerFooterOnClickListener); 716 mAgendaListView.addFooterView(mFooterView); 717 mDoneSettingUpHeaderFooter = true; 718 } 719 synchronized (mQueryQueue) { 720 int totalAgendaRangeStart = -1; 721 int totalAgendaRangeEnd = -1; 722 723 if (cursorSize != 0) { 724 // Remove the query that just completed 725 QuerySpec x = mQueryQueue.poll(); 726 if (BASICLOG && !x.equals(data)) { 727 Log.e(TAG, "onQueryComplete - cookie != head of queue"); 728 } 729 mEmptyCursorCount = 0; 730 if (data.queryType == QUERY_TYPE_NEWER) { 731 mNewerRequestsProcessed++; 732 } else if (data.queryType == QUERY_TYPE_OLDER) { 733 mOlderRequestsProcessed++; 734 } 735 736 totalAgendaRangeStart = mAdapterInfos.getFirst().start; 737 totalAgendaRangeEnd = mAdapterInfos.getLast().end; 738 } else { // CursorSize == 0 739 QuerySpec querySpec = mQueryQueue.peek(); 740 741 // Update Adapter Info with new start and end date range 742 if (!mAdapterInfos.isEmpty()) { 743 DayAdapterInfo first = mAdapterInfos.getFirst(); 744 DayAdapterInfo last = mAdapterInfos.getLast(); 745 746 if (first.start - 1 <= querySpec.end && querySpec.start < first.start) { 747 first.start = querySpec.start; 748 } 749 750 if (querySpec.start <= last.end + 1 && last.end < querySpec.end) { 751 last.end = querySpec.end; 752 } 753 754 totalAgendaRangeStart = first.start; 755 totalAgendaRangeEnd = last.end; 756 } else { 757 totalAgendaRangeStart = querySpec.start; 758 totalAgendaRangeEnd = querySpec.end; 759 } 760 761 // Update query specification with expanded search range 762 // and maybe rerun query 763 switch (querySpec.queryType) { 764 case QUERY_TYPE_OLDER: 765 totalAgendaRangeStart = querySpec.start; 766 querySpec.start -= MAX_QUERY_DURATION; 767 break; 768 case QUERY_TYPE_NEWER: 769 totalAgendaRangeEnd = querySpec.end; 770 querySpec.end += MAX_QUERY_DURATION; 771 break; 772 case QUERY_TYPE_CLEAN: 773 totalAgendaRangeStart = querySpec.start; 774 totalAgendaRangeEnd = querySpec.end; 775 querySpec.start -= MAX_QUERY_DURATION / 2; 776 querySpec.end += MAX_QUERY_DURATION / 2; 777 break; 778 } 779 780 if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) { 781 // Nothing in the cursor again. Dropping query 782 mQueryQueue.poll(); 783 } 784 } 785 786 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd); 787 788 // Fire off the next query if any 789 Iterator<QuerySpec> it = mQueryQueue.iterator(); 790 while (it.hasNext()) { 791 QuerySpec queryData = it.next(); 792 if (!isInRange(queryData.start, queryData.end)) { 793 // Query accepted 794 if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size()); 795 doQuery(queryData); 796 break; 797 } else { 798 // Query rejected 799 it.remove(); 800 if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size()); 801 } 802 } 803 } 804 if (BASICLOG) { 805 for (DayAdapterInfo info3 : mAdapterInfos) { 806 Log.e(TAG, "> " + info3.toString()); 807 } 808 } 809 } 810 811 /* 812 * Update the adapter info array with a the new cursor. Close out old 813 * cursors as needed. 814 * 815 * @return number of rows removed from the beginning 816 */ 817 private int processNewCursor(QuerySpec data, Cursor cursor) { 818 synchronized (mAdapterInfos) { 819 // Remove adapter info's from adapterInfos as needed 820 DayAdapterInfo info = pruneAdapterInfo(data.queryType); 821 int listPositionOffset = 0; 822 if (info == null) { 823 info = new DayAdapterInfo(mContext); 824 } else { 825 if (DEBUGLOG) 826 Log.e(TAG, "processNewCursor listPositionOffsetA=" 827 + -info.size); 828 listPositionOffset = -info.size; 829 } 830 831 // Setup adapter info 832 info.start = data.start; 833 info.end = data.end; 834 info.cursor = cursor; 835 info.dayAdapter.changeCursor(info); 836 info.size = info.dayAdapter.getCount(); 837 838 // Insert into adapterInfos 839 if (mAdapterInfos.isEmpty() 840 || data.end <= mAdapterInfos.getFirst().start) { 841 mAdapterInfos.addFirst(info); 842 listPositionOffset += info.size; 843 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) { 844 mAdapterInfos.addLast(info); 845 for (DayAdapterInfo info2 : mAdapterInfos) { 846 Log.e("========== BUG ==", info2.toString()); 847 } 848 } else { 849 mAdapterInfos.addLast(info); 850 } 851 852 // Update offsets in adapterInfos 853 mRowCount = 0; 854 for (DayAdapterInfo info3 : mAdapterInfos) { 855 info3.offset = mRowCount; 856 mRowCount += info3.size; 857 } 858 mLastUsedInfo = null; 859 860 return listPositionOffset; 861 } 862 } 863 } 864 865 static String getViewTitle(View x) { 866 String title = ""; 867 if (x != null) { 868 Object yy = x.getTag(); 869 if (yy instanceof AgendaAdapter.ViewHolder) { 870 TextView tv = ((AgendaAdapter.ViewHolder) yy).title; 871 if (tv != null) { 872 title = (String) tv.getText(); 873 } 874 } else if (yy != null) { 875 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView; 876 if (dateView != null) { 877 title = (String) dateView.getText(); 878 } 879 } 880 } 881 return title; 882 } 883 884 public void setHideDeclinedEvents(boolean hideDeclined) { 885 mHideDeclined = hideDeclined; 886 } 887 } 888