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