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