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