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