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