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