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 17 package com.android.calendar.agenda; 18 19 import android.content.AsyncQueryHandler; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.provider.CalendarContract; 28 import android.provider.CalendarContract.Attendees; 29 import android.provider.CalendarContract.Calendars; 30 import android.provider.CalendarContract.Instances; 31 import android.text.format.DateUtils; 32 import android.text.format.Time; 33 import android.util.Log; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.View.OnClickListener; 37 import android.view.ViewGroup; 38 import android.widget.AbsListView.OnScrollListener; 39 import android.widget.BaseAdapter; 40 import android.widget.GridLayout; 41 import android.widget.TextView; 42 43 import com.android.calendar.CalendarController; 44 import com.android.calendar.CalendarController.EventType; 45 import com.android.calendar.CalendarController.ViewType; 46 import com.android.calendar.R; 47 import com.android.calendar.StickyHeaderListView; 48 import com.android.calendar.Utils; 49 50 import java.util.Formatter; 51 import java.util.Iterator; 52 import java.util.LinkedList; 53 import java.util.Locale; 54 import java.util.concurrent.ConcurrentLinkedQueue; 55 56 /* 57 Bugs Bugs Bugs: 58 - At rotation and launch time, the initial position is not set properly. This code is calling 59 listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one. 60 - Scroll using trackball isn't repositioning properly after a new adapter is added. 61 - Track ball clicks at the header/footer doesn't work. 62 - Potential ping pong effect if the prefetch window is big and data is limited 63 - Add index in calendar provider 64 65 ToDo ToDo ToDo: 66 Get design of header and footer from designer 67 68 Make scrolling smoother. 69 Test for correctness 70 Loading speed 71 Check for leaks and excessive allocations 72 */ 73 74 public class AgendaWindowAdapter extends BaseAdapter 75 implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener{ 76 77 static final boolean BASICLOG = false; 78 static final boolean DEBUGLOG = false; 79 private static final String TAG = "AgendaWindowAdapter"; 80 81 private static final String AGENDA_SORT_ORDER = 82 CalendarContract.Instances.START_DAY + " ASC, " + 83 CalendarContract.Instances.BEGIN + " ASC, " + 84 CalendarContract.Events.TITLE + " ASC"; 85 86 public static final int INDEX_INSTANCE_ID = 0; 87 public static final int INDEX_TITLE = 1; 88 public static final int INDEX_EVENT_LOCATION = 2; 89 public static final int INDEX_ALL_DAY = 3; 90 public static final int INDEX_HAS_ALARM = 4; 91 public static final int INDEX_COLOR = 5; 92 public static final int INDEX_RRULE = 6; 93 public static final int INDEX_BEGIN = 7; 94 public static final int INDEX_END = 8; 95 public static final int INDEX_EVENT_ID = 9; 96 public static final int INDEX_START_DAY = 10; 97 public static final int INDEX_END_DAY = 11; 98 public static final int INDEX_SELF_ATTENDEE_STATUS = 12; 99 public static final int INDEX_ORGANIZER = 13; 100 public static final int INDEX_OWNER_ACCOUNT = 14; 101 public static final int INDEX_CAN_ORGANIZER_RESPOND= 15; 102 public static final int INDEX_TIME_ZONE = 16; 103 104 private static final String[] PROJECTION = new String[] { 105 Instances._ID, // 0 106 Instances.TITLE, // 1 107 Instances.EVENT_LOCATION, // 2 108 Instances.ALL_DAY, // 3 109 Instances.HAS_ALARM, // 4 110 Instances.DISPLAY_COLOR, // 5 If SDK < 16, set to Instances.CALENDAR_COLOR. 111 Instances.RRULE, // 6 112 Instances.BEGIN, // 7 113 Instances.END, // 8 114 Instances.EVENT_ID, // 9 115 Instances.START_DAY, // 10 Julian start day 116 Instances.END_DAY, // 11 Julian end day 117 Instances.SELF_ATTENDEE_STATUS, // 12 118 Instances.ORGANIZER, // 13 119 Instances.OWNER_ACCOUNT, // 14 120 Instances.CAN_ORGANIZER_RESPOND, // 15 121 Instances.EVENT_TIMEZONE, // 16 122 }; 123 124 static { 125 if (!Utils.isJellybeanOrLater()) { 126 PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR; 127 } 128 } 129 130 // Listview may have a bug where the index/position is not consistent when there's a header. 131 // position == positionInListView - OFF_BY_ONE_BUG 132 // TODO Need to look into this. 133 private static final int OFF_BY_ONE_BUG = 1; 134 private static final int MAX_NUM_OF_ADAPTERS = 5; 135 private static final int IDEAL_NUM_OF_EVENTS = 50; 136 private static final int MIN_QUERY_DURATION = 7; // days 137 private static final int MAX_QUERY_DURATION = 60; // days 138 private static final int PREFETCH_BOUNDARY = 1; 139 140 /** Times to auto-expand/retry query after getting no data */ 141 private static final int RETRIES_ON_NO_DATA = 1; 142 143 private final Context mContext; 144 private final Resources mResources; 145 private final QueryHandler mQueryHandler; 146 private final AgendaListView mAgendaListView; 147 148 /** The sum of the rows in all the adapters */ 149 private int mRowCount; 150 151 /** The number of times we have queried and gotten no results back */ 152 private int mEmptyCursorCount; 153 154 /** Cached value of the last used adapter */ 155 private DayAdapterInfo mLastUsedInfo; 156 157 private final LinkedList<DayAdapterInfo> mAdapterInfos = 158 new LinkedList<DayAdapterInfo>(); 159 private final ConcurrentLinkedQueue<QuerySpec> mQueryQueue = 160 new ConcurrentLinkedQueue<QuerySpec>(); 161 private final TextView mHeaderView; 162 private final TextView mFooterView; 163 private boolean mDoneSettingUpHeaderFooter = false; 164 165 private final boolean mIsTabletConfig; 166 167 boolean mCleanQueryInitiated = false; 168 private int mStickyHeaderSize = 44; // Initial size big enough for it to work 169 170 /** 171 * When the user scrolled to the top, a query will be made for older events 172 * and this will be incremented. Don't make more requests if 173 * mOlderRequests > mOlderRequestsProcessed. 174 */ 175 private int mOlderRequests; 176 177 /** Number of "older" query that has been processed. */ 178 private int mOlderRequestsProcessed; 179 180 /** 181 * When the user scrolled to the bottom, a query will be made for newer 182 * events and this will be incremented. Don't make more requests if 183 * mNewerRequests > mNewerRequestsProcessed. 184 */ 185 private int mNewerRequests; 186 187 /** Number of "newer" query that has been processed. */ 188 private int mNewerRequestsProcessed; 189 190 // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread. 191 private final Formatter mFormatter; 192 private final StringBuilder mStringBuilder; 193 private String mTimeZone; 194 195 // defines if to pop-up the current event when the agenda is first shown 196 private final boolean mShowEventOnStart; 197 198 private final Runnable mTZUpdater = new Runnable() { 199 @Override 200 public void run() { 201 mTimeZone = Utils.getTimeZone(mContext, this); 202 notifyDataSetChanged(); 203 } 204 }; 205 206 private final Handler mDataChangedHandler = new Handler(); 207 private final Runnable mDataChangedRunnable = new Runnable() { 208 @Override 209 public void run() { 210 notifyDataSetChanged(); 211 } 212 }; 213 214 private boolean mShuttingDown; 215 private boolean mHideDeclined; 216 217 // Used to stop a fling motion if the ListView is set to a specific position 218 int mListViewScrollState = OnScrollListener.SCROLL_STATE_IDLE; 219 220 /** The current search query, or null if none */ 221 private String mSearchQuery; 222 223 private long mSelectedInstanceId = -1; 224 225 private final int mSelectedItemBackgroundColor; 226 private final int mSelectedItemTextColor; 227 private final float mItemRightMargin; 228 229 // Types of Query 230 private static final int QUERY_TYPE_OLDER = 0; // Query for older events 231 private static final int QUERY_TYPE_NEWER = 1; // Query for newer events 232 private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date 233 234 private static class QuerySpec { 235 long queryStartMillis; 236 Time goToTime; 237 int start; 238 int end; 239 String searchQuery; 240 int queryType; 241 long id; 242 243 public QuerySpec(int queryType) { 244 this.queryType = queryType; 245 id = -1; 246 } 247 248 @Override 249 public int hashCode() { 250 final int prime = 31; 251 int result = 1; 252 result = prime * result + end; 253 result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32)); 254 result = prime * result + queryType; 255 result = prime * result + start; 256 if (searchQuery != null) { 257 result = prime * result + searchQuery.hashCode(); 258 } 259 if (goToTime != null) { 260 long goToTimeMillis = goToTime.toMillis(false); 261 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32)); 262 } 263 result = prime * result + (int)id; 264 return result; 265 } 266 267 @Override 268 public boolean equals(Object obj) { 269 if (this == obj) return true; 270 if (obj == null) return false; 271 if (getClass() != obj.getClass()) return false; 272 QuerySpec other = (QuerySpec) obj; 273 if (end != other.end || queryStartMillis != other.queryStartMillis 274 || queryType != other.queryType || start != other.start 275 || Utils.equals(searchQuery, other.searchQuery) || id != other.id) { 276 return false; 277 } 278 279 if (goToTime != null) { 280 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) { 281 return false; 282 } 283 } else { 284 if (other.goToTime != null) { 285 return false; 286 } 287 } 288 return true; 289 } 290 } 291 292 static class EventInfo { 293 long begin; 294 long end; 295 long id; 296 int startDay; 297 boolean allDay; 298 } 299 300 static class DayAdapterInfo { 301 Cursor cursor; 302 AgendaByDayAdapter dayAdapter; 303 int start; // start day of the cursor's coverage 304 int end; // end day of the cursor's coverage 305 int offset; // offset in position in the list view 306 int size; // dayAdapter.getCount() 307 308 public DayAdapterInfo(Context context) { 309 dayAdapter = new AgendaByDayAdapter(context); 310 } 311 312 @Override 313 public String toString() { 314 // Static class, so the time in this toString will not reflect the 315 // home tz settings. This should only affect debugging. 316 Time time = new Time(); 317 StringBuilder sb = new StringBuilder(); 318 time.setJulianDay(start); 319 time.normalize(false); 320 sb.append("Start:").append(time.toString()); 321 time.setJulianDay(end); 322 time.normalize(false); 323 sb.append(" End:").append(time.toString()); 324 sb.append(" Offset:").append(offset); 325 sb.append(" Size:").append(size); 326 return sb.toString(); 327 } 328 } 329 330 public AgendaWindowAdapter(Context context, 331 AgendaListView agendaListView, boolean showEventOnStart) { 332 mContext = context; 333 mResources = context.getResources(); 334 mSelectedItemBackgroundColor = mResources 335 .getColor(R.color.agenda_selected_background_color); 336 mSelectedItemTextColor = mResources.getColor(R.color.agenda_selected_text_color); 337 mItemRightMargin = mResources.getDimension(R.dimen.agenda_item_right_margin); 338 mIsTabletConfig = Utils.getConfigBool(mContext, R.bool.tablet_config); 339 340 mTimeZone = Utils.getTimeZone(context, mTZUpdater); 341 mAgendaListView = agendaListView; 342 mQueryHandler = new QueryHandler(context.getContentResolver()); 343 344 mStringBuilder = new StringBuilder(50); 345 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 346 347 mShowEventOnStart = showEventOnStart; 348 349 // Implies there is no sticky header 350 if (!mShowEventOnStart) { 351 mStickyHeaderSize = 0; 352 } 353 mSearchQuery = null; 354 355 LayoutInflater inflater = (LayoutInflater) context 356 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 357 mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 358 mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null); 359 mHeaderView.setText(R.string.loading); 360 mAgendaListView.addHeaderView(mHeaderView); 361 } 362 363 // Method in Adapter 364 @Override 365 public int getViewTypeCount() { 366 return AgendaByDayAdapter.TYPE_LAST; 367 } 368 369 // Method in BaseAdapter 370 @Override 371 public boolean areAllItemsEnabled() { 372 return false; 373 } 374 375 // Method in Adapter 376 @Override 377 public int getItemViewType(int position) { 378 DayAdapterInfo info = getAdapterInfoByPosition(position); 379 if (info != null) { 380 return info.dayAdapter.getItemViewType(position - info.offset); 381 } else { 382 return -1; 383 } 384 } 385 386 // Method in BaseAdapter 387 @Override 388 public boolean isEnabled(int position) { 389 DayAdapterInfo info = getAdapterInfoByPosition(position); 390 if (info != null) { 391 return info.dayAdapter.isEnabled(position - info.offset); 392 } else { 393 return false; 394 } 395 } 396 397 // Abstract Method in BaseAdapter 398 public int getCount() { 399 return mRowCount; 400 } 401 402 // Abstract Method in BaseAdapter 403 public Object getItem(int position) { 404 DayAdapterInfo info = getAdapterInfoByPosition(position); 405 if (info != null) { 406 return info.dayAdapter.getItem(position - info.offset); 407 } else { 408 return null; 409 } 410 } 411 412 // Method in BaseAdapter 413 @Override 414 public boolean hasStableIds() { 415 return true; 416 } 417 418 // Abstract Method in BaseAdapter 419 @Override 420 public long getItemId(int position) { 421 DayAdapterInfo info = getAdapterInfoByPosition(position); 422 if (info != null) { 423 int curPos = info.dayAdapter.getCursorPosition(position - info.offset); 424 if (curPos == Integer.MIN_VALUE) { 425 return -1; 426 } 427 // Regular event 428 if (curPos >= 0) { 429 info.cursor.moveToPosition(curPos); 430 return info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID) << 20 + 431 info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 432 } 433 // Day Header 434 return info.dayAdapter.findJulianDayFromPosition(position); 435 436 } else { 437 return -1; 438 } 439 } 440 441 // Abstract Method in BaseAdapter 442 public View getView(int position, View convertView, ViewGroup parent) { 443 if (position >= (mRowCount - PREFETCH_BOUNDARY) 444 && mNewerRequests <= mNewerRequestsProcessed) { 445 if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: "); 446 mNewerRequests++; 447 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 448 } 449 450 if (position < PREFETCH_BOUNDARY 451 && mOlderRequests <= mOlderRequestsProcessed) { 452 if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: "); 453 mOlderRequests++; 454 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 455 } 456 457 final View v; 458 DayAdapterInfo info = getAdapterInfoByPosition(position); 459 if (info != null) { 460 int offset = position - info.offset; 461 v = info.dayAdapter.getView(offset, convertView, 462 parent); 463 464 // Turn on the past/present separator if the view is a day header 465 // and it is the first day with events after yesterday. 466 if (info.dayAdapter.isDayHeaderView(offset)) { 467 View simpleDivider = v.findViewById(R.id.top_divider_simple); 468 View pastPresentDivider = v.findViewById(R.id.top_divider_past_present); 469 if (info.dayAdapter.isFirstDayAfterYesterday(offset)) { 470 if (simpleDivider != null && pastPresentDivider != null) { 471 simpleDivider.setVisibility(View.GONE); 472 pastPresentDivider.setVisibility(View.VISIBLE); 473 } 474 } else if (simpleDivider != null && pastPresentDivider != null) { 475 simpleDivider.setVisibility(View.VISIBLE); 476 pastPresentDivider.setVisibility(View.GONE); 477 } 478 } 479 } else { 480 // TODO 481 Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position); 482 TextView tv = new TextView(mContext); 483 tv.setText("Bug! " + position); 484 v = tv; 485 } 486 487 // If this is not a tablet config don't do selection highlighting 488 if (!mIsTabletConfig) { 489 return v; 490 } 491 // Show selected marker if this is item is selected 492 boolean selected = false; 493 Object yy = v.getTag(); 494 if (yy instanceof AgendaAdapter.ViewHolder) { 495 AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) yy; 496 selected = mSelectedInstanceId == vh.instanceId; 497 vh.selectedMarker.setVisibility((selected && mShowEventOnStart) ? 498 View.VISIBLE : View.GONE); 499 if (mShowEventOnStart) { 500 GridLayout.LayoutParams lp = 501 (GridLayout.LayoutParams)vh.textContainer.getLayoutParams(); 502 if (selected) { 503 mSelectedVH = vh; 504 v.setBackgroundColor(mSelectedItemBackgroundColor); 505 vh.title.setTextColor(mSelectedItemTextColor); 506 vh.when.setTextColor(mSelectedItemTextColor); 507 vh.where.setTextColor(mSelectedItemTextColor); 508 lp.setMargins(0, 0, 0, 0); 509 vh.textContainer.setLayoutParams(lp); 510 } else { 511 lp.setMargins(0, 0, (int)mItemRightMargin, 0); 512 vh.textContainer.setLayoutParams(lp); 513 } 514 } 515 } 516 517 if (DEBUGLOG) { 518 Log.e(TAG, "getView " + position + " = " + getViewTitle(v)); 519 } 520 return v; 521 } 522 523 private AgendaAdapter.ViewHolder mSelectedVH = null; 524 525 private int findEventPositionNearestTime(Time time, long id) { 526 DayAdapterInfo info = getAdapterInfoByTime(time); 527 int pos = -1; 528 if (info != null) { 529 pos = info.offset + info.dayAdapter.findEventPositionNearestTime(time, id); 530 } 531 if (DEBUGLOG) Log.e(TAG, "findEventPositionNearestTime " + time + " id:" + id + " =" + pos); 532 return pos; 533 } 534 535 protected DayAdapterInfo getAdapterInfoByPosition(int position) { 536 synchronized (mAdapterInfos) { 537 if (mLastUsedInfo != null && mLastUsedInfo.offset <= position 538 && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) { 539 return mLastUsedInfo; 540 } 541 for (DayAdapterInfo info : mAdapterInfos) { 542 if (info.offset <= position 543 && position < (info.offset + info.size)) { 544 mLastUsedInfo = info; 545 return info; 546 } 547 } 548 } 549 return null; 550 } 551 552 private DayAdapterInfo getAdapterInfoByTime(Time time) { 553 if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString()); 554 555 Time tmpTime = new Time(time); 556 long timeInMillis = tmpTime.normalize(true); 557 int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff); 558 synchronized (mAdapterInfos) { 559 for (DayAdapterInfo info : mAdapterInfos) { 560 if (info.start <= day && day <= info.end) { 561 return info; 562 } 563 } 564 } 565 return null; 566 } 567 568 public EventInfo getEventByPosition(final int positionInListView) { 569 return getEventByPosition(positionInListView, true); 570 } 571 572 /** 573 * Return the event info for a given position in the adapter 574 * @param positionInListView 575 * @param returnEventStartDay If true, return actual event startday. Otherwise 576 * return agenda date-header date as the startDay. 577 * The two will differ for multi-day events after the first day. 578 * @return 579 */ 580 public EventInfo getEventByPosition(final int positionInListView, 581 boolean returnEventStartDay) { 582 if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + positionInListView); 583 if (positionInListView < 0) { 584 return null; 585 } 586 587 final int positionInAdapter = positionInListView - OFF_BY_ONE_BUG; 588 DayAdapterInfo info = getAdapterInfoByPosition(positionInAdapter); 589 if (info == null) { 590 return null; 591 } 592 593 int cursorPosition = info.dayAdapter.getCursorPosition(positionInAdapter - info.offset); 594 if (cursorPosition == Integer.MIN_VALUE) { 595 return null; 596 } 597 598 boolean isDayHeader = false; 599 if (cursorPosition < 0) { 600 cursorPosition = -cursorPosition; 601 isDayHeader = true; 602 } 603 604 if (cursorPosition < info.cursor.getCount()) { 605 EventInfo ei = buildEventInfoFromCursor(info.cursor, cursorPosition, isDayHeader); 606 if (!returnEventStartDay && !isDayHeader) { 607 ei.startDay = info.dayAdapter.findJulianDayFromPosition(positionInAdapter - 608 info.offset); 609 } 610 return ei; 611 } 612 return null; 613 } 614 615 private EventInfo buildEventInfoFromCursor(final Cursor cursor, int cursorPosition, 616 boolean isDayHeader) { 617 if (cursorPosition == -1) { 618 cursor.moveToFirst(); 619 } else { 620 cursor.moveToPosition(cursorPosition); 621 } 622 EventInfo event = new EventInfo(); 623 event.begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); 624 event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END); 625 event.startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY); 626 627 event.allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 628 if (event.allDay) { // UTC 629 Time time = new Time(mTimeZone); 630 time.setJulianDay(Time.getJulianDay(event.begin, 0)); 631 event.begin = time.toMillis(false /* use isDst */); 632 } else if (isDayHeader) { // Trim to midnight. 633 Time time = new Time(mTimeZone); 634 time.set(event.begin); 635 time.hour = 0; 636 time.minute = 0; 637 time.second = 0; 638 event.begin = time.toMillis(false /* use isDst */); 639 } 640 641 if (!isDayHeader) { 642 if (event.allDay) { 643 Time time = new Time(mTimeZone); 644 time.setJulianDay(Time.getJulianDay(event.end, 0)); 645 event.end = time.toMillis(false /* use isDst */); 646 } else { 647 event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END); 648 } 649 650 event.id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID); 651 } 652 return event; 653 } 654 655 public void refresh(Time goToTime, long id, String searchQuery, boolean forced, 656 boolean refreshEventInfo) { 657 if (searchQuery != null) { 658 mSearchQuery = searchQuery; 659 } 660 661 if (DEBUGLOG) { 662 Log.e(TAG, this + ": refresh " + goToTime.toString() + " id " + id 663 + ((searchQuery != null) ? searchQuery : "") 664 + (forced ? " forced" : " not forced") 665 + (refreshEventInfo ? " refresh event info" : "")); 666 } 667 668 int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff); 669 670 if (!forced && isInRange(startDay, startDay)) { 671 // No need to re-query 672 if (!mAgendaListView.isEventVisible(goToTime, id)) { 673 int gotoPosition = findEventPositionNearestTime(goToTime, id); 674 if (gotoPosition > 0) { 675 mAgendaListView.setSelectionFromTop(gotoPosition + 676 OFF_BY_ONE_BUG, mStickyHeaderSize); 677 if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) { 678 mAgendaListView.smoothScrollBy(0, 0); 679 } 680 if (refreshEventInfo) { 681 long newInstanceId = findInstanceIdFromPosition(gotoPosition); 682 if (newInstanceId != getSelectedInstanceId()) { 683 setSelectedInstanceId(newInstanceId); 684 mDataChangedHandler.post(mDataChangedRunnable); 685 Cursor tempCursor = getCursorByPosition(gotoPosition); 686 if (tempCursor != null) { 687 int tempCursorPosition = getCursorPositionByPosition(gotoPosition); 688 EventInfo event = 689 buildEventInfoFromCursor(tempCursor, tempCursorPosition, 690 false); 691 CalendarController.getInstance(mContext) 692 .sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, 693 event.id, event.begin, event.end, 0, 694 0, CalendarController.EventInfo.buildViewExtraLong( 695 Attendees.ATTENDEE_STATUS_NONE, 696 event.allDay), goToTime.toMillis(false)); 697 } 698 } 699 } 700 } 701 702 Time actualTime = new Time(mTimeZone); 703 actualTime.set(goToTime); 704 CalendarController.getInstance(mContext).sendEvent(this, EventType.UPDATE_TITLE, 705 actualTime, actualTime, -1, ViewType.CURRENT); 706 } 707 return; 708 } 709 710 // If AllInOneActivity is sending a second GOTO event(in OnResume), ignore it. 711 if (!mCleanQueryInitiated || searchQuery != null) { 712 // Query for a total of MIN_QUERY_DURATION days 713 int endDay = startDay + MIN_QUERY_DURATION; 714 715 mSelectedInstanceId = -1; 716 mCleanQueryInitiated = true; 717 queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN, id); 718 719 // Pre-fetch more data to overcome a race condition in AgendaListView.shiftSelection 720 // Queuing more data with the goToTime set to the selected time skips the call to 721 // shiftSelection on refresh. 722 mOlderRequests++; 723 queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_OLDER, id); 724 mNewerRequests++; 725 queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_NEWER, id); 726 } 727 } 728 729 public void close() { 730 mShuttingDown = true; 731 pruneAdapterInfo(QUERY_TYPE_CLEAN); 732 if (mQueryHandler != null) { 733 mQueryHandler.cancelOperation(0); 734 } 735 } 736 737 private DayAdapterInfo pruneAdapterInfo(int queryType) { 738 synchronized (mAdapterInfos) { 739 DayAdapterInfo recycleMe = null; 740 if (!mAdapterInfos.isEmpty()) { 741 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) { 742 if (queryType == QUERY_TYPE_NEWER) { 743 recycleMe = mAdapterInfos.removeFirst(); 744 } else if (queryType == QUERY_TYPE_OLDER) { 745 recycleMe = mAdapterInfos.removeLast(); 746 // Keep the size only if the oldest items are removed. 747 recycleMe.size = 0; 748 } 749 if (recycleMe != null) { 750 if (recycleMe.cursor != null) { 751 recycleMe.cursor.close(); 752 } 753 return recycleMe; 754 } 755 } 756 757 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) { 758 mRowCount = 0; 759 int deletedRows = 0; 760 DayAdapterInfo info; 761 do { 762 info = mAdapterInfos.poll(); 763 if (info != null) { 764 // TODO the following causes ANR's. Do this in a thread. 765 info.cursor.close(); 766 deletedRows += info.size; 767 recycleMe = info; 768 } 769 } while (info != null); 770 771 if (recycleMe != null) { 772 recycleMe.cursor = null; 773 recycleMe.size = deletedRows; 774 } 775 } 776 } 777 return recycleMe; 778 } 779 } 780 781 private String buildQuerySelection() { 782 // Respect the preference to show/hide declined events 783 784 if (mHideDeclined) { 785 return Calendars.VISIBLE + "=1 AND " 786 + Instances.SELF_ATTENDEE_STATUS + "!=" 787 + Attendees.ATTENDEE_STATUS_DECLINED; 788 } else { 789 return Calendars.VISIBLE + "=1"; 790 } 791 } 792 793 private Uri buildQueryUri(int start, int end, String searchQuery) { 794 Uri rootUri = searchQuery == null ? 795 Instances.CONTENT_BY_DAY_URI : 796 Instances.CONTENT_SEARCH_BY_DAY_URI; 797 Uri.Builder builder = rootUri.buildUpon(); 798 ContentUris.appendId(builder, start); 799 ContentUris.appendId(builder, end); 800 if (searchQuery != null) { 801 builder.appendPath(searchQuery); 802 } 803 return builder.build(); 804 } 805 806 private boolean isInRange(int start, int end) { 807 synchronized (mAdapterInfos) { 808 if (mAdapterInfos.isEmpty()) { 809 return false; 810 } 811 return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end; 812 } 813 } 814 815 private int calculateQueryDuration(int start, int end) { 816 int queryDuration = MAX_QUERY_DURATION; 817 if (mRowCount != 0) { 818 queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount; 819 } 820 821 if (queryDuration > MAX_QUERY_DURATION) { 822 queryDuration = MAX_QUERY_DURATION; 823 } else if (queryDuration < MIN_QUERY_DURATION) { 824 queryDuration = MIN_QUERY_DURATION; 825 } 826 827 return queryDuration; 828 } 829 830 private boolean queueQuery(int start, int end, Time goToTime, 831 String searchQuery, int queryType, long id) { 832 QuerySpec queryData = new QuerySpec(queryType); 833 queryData.goToTime = goToTime; 834 queryData.start = start; 835 queryData.end = end; 836 queryData.searchQuery = searchQuery; 837 queryData.id = id; 838 return queueQuery(queryData); 839 } 840 841 private boolean queueQuery(QuerySpec queryData) { 842 queryData.searchQuery = mSearchQuery; 843 Boolean queuedQuery; 844 synchronized (mQueryQueue) { 845 queuedQuery = false; 846 Boolean doQueryNow = mQueryQueue.isEmpty(); 847 mQueryQueue.add(queryData); 848 queuedQuery = true; 849 if (doQueryNow) { 850 doQuery(queryData); 851 } 852 } 853 return queuedQuery; 854 } 855 856 private void doQuery(QuerySpec queryData) { 857 if (!mAdapterInfos.isEmpty()) { 858 int start = mAdapterInfos.getFirst().start; 859 int end = mAdapterInfos.getLast().end; 860 int queryDuration = calculateQueryDuration(start, end); 861 switch(queryData.queryType) { 862 case QUERY_TYPE_OLDER: 863 queryData.end = start - 1; 864 queryData.start = queryData.end - queryDuration; 865 break; 866 case QUERY_TYPE_NEWER: 867 queryData.start = end + 1; 868 queryData.end = queryData.start + queryDuration; 869 break; 870 } 871 872 // By "compacting" cursors, this fixes the disco/ping-pong problem 873 // b/5311977 874 if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) { 875 if (DEBUGLOG) { 876 Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount 877 + " totalStart:" + start 878 + " totalEnd:" + end 879 + " query.start:" + queryData.start 880 + " query.end:" + queryData.end); 881 } 882 883 queryData.queryType = QUERY_TYPE_CLEAN; 884 885 if (queryData.start > start) { 886 queryData.start = start; 887 } 888 if (queryData.end < end) { 889 queryData.end = end; 890 } 891 } 892 } 893 894 if (BASICLOG) { 895 Time time = new Time(mTimeZone); 896 time.setJulianDay(queryData.start); 897 Time time2 = new Time(mTimeZone); 898 time2.setJulianDay(queryData.end); 899 Log.v(TAG, "startQuery: " + time.toString() + " to " 900 + time2.toString() + " then go to " + queryData.goToTime); 901 } 902 903 mQueryHandler.cancelOperation(0); 904 if (BASICLOG) queryData.queryStartMillis = System.nanoTime(); 905 906 Uri queryUri = buildQueryUri( 907 queryData.start, queryData.end, queryData.searchQuery); 908 mQueryHandler.startQuery(0, queryData, queryUri, 909 PROJECTION, buildQuerySelection(), null, 910 AGENDA_SORT_ORDER); 911 } 912 913 private String formatDateString(int julianDay) { 914 Time time = new Time(mTimeZone); 915 time.setJulianDay(julianDay); 916 long millis = time.toMillis(false); 917 mStringBuilder.setLength(0); 918 return DateUtils.formatDateRange(mContext, mFormatter, millis, millis, 919 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE 920 | DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString(); 921 } 922 923 private void updateHeaderFooter(final int start, final int end) { 924 mHeaderView.setText(mContext.getString(R.string.show_older_events, 925 formatDateString(start))); 926 mFooterView.setText(mContext.getString(R.string.show_newer_events, 927 formatDateString(end))); 928 } 929 930 private class QueryHandler extends AsyncQueryHandler { 931 932 public QueryHandler(ContentResolver cr) { 933 super(cr); 934 } 935 936 @Override 937 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 938 QuerySpec data = (QuerySpec)cookie; 939 if (BASICLOG) { 940 long queryEndMillis = System.nanoTime(); 941 Log.e(TAG, "Query time(ms): " 942 + (queryEndMillis - data.queryStartMillis) / 1000000 943 + " Count: " + cursor.getCount()); 944 } 945 946 if (data.queryType == QUERY_TYPE_CLEAN) { 947 mCleanQueryInitiated = false; 948 } 949 950 if (mShuttingDown) { 951 cursor.close(); 952 return; 953 } 954 955 // Notify Listview of changes and update position 956 int cursorSize = cursor.getCount(); 957 if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) { 958 final int listPositionOffset = processNewCursor(data, cursor); 959 int newPosition = -1; 960 if (data.goToTime == null) { // Typical Scrolling type query 961 notifyDataSetChanged(); 962 if (listPositionOffset != 0) { 963 mAgendaListView.shiftSelection(listPositionOffset); 964 } 965 } else { // refresh() called. Go to the designated position 966 final Time goToTime = data.goToTime; 967 notifyDataSetChanged(); 968 newPosition = findEventPositionNearestTime(goToTime, data.id); 969 if (newPosition >= 0) { 970 if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) { 971 mAgendaListView.smoothScrollBy(0, 0); 972 } 973 mAgendaListView.setSelectionFromTop(newPosition + OFF_BY_ONE_BUG, 974 mStickyHeaderSize); 975 Time actualTime = new Time(mTimeZone); 976 actualTime.set(goToTime); 977 CalendarController.getInstance(mContext).sendEvent(this, 978 EventType.UPDATE_TITLE, actualTime, actualTime, -1, 979 ViewType.CURRENT); 980 } 981 if (DEBUGLOG) { 982 Log.e(TAG, "Setting listview to " + 983 "findEventPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG)); 984 } 985 } 986 987 // Make sure we change the selected instance Id only on a clean query and we 988 // do not have one set already 989 if (mSelectedInstanceId == -1 && newPosition != -1 && 990 data.queryType == QUERY_TYPE_CLEAN) { 991 if (data.id != -1 || data.goToTime != null) { 992 mSelectedInstanceId = findInstanceIdFromPosition(newPosition); 993 } 994 } 995 996 // size == 1 means a fresh query. Possibly after the data changed. 997 // Let's check whether mSelectedInstanceId is still valid. 998 if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) { 999 boolean found = false; 1000 cursor.moveToPosition(-1); 1001 while (cursor.moveToNext()) { 1002 if (mSelectedInstanceId == cursor 1003 .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) { 1004 found = true; 1005 break; 1006 } 1007 }; 1008 1009 if (!found) { 1010 mSelectedInstanceId = -1; 1011 } 1012 } 1013 1014 // Show the requested event 1015 if (mShowEventOnStart && data.queryType == QUERY_TYPE_CLEAN) { 1016 Cursor tempCursor = null; 1017 int tempCursorPosition = -1; 1018 1019 // If no valid event is selected , just pick the first one 1020 if (mSelectedInstanceId == -1) { 1021 if (cursor.moveToFirst()) { 1022 mSelectedInstanceId = cursor 1023 .getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID); 1024 // Set up a dummy view holder so we have the right all day 1025 // info when the view is created. 1026 // TODO determine the full set of what might be useful to 1027 // know about the selected view and fill it in. 1028 mSelectedVH = new AgendaAdapter.ViewHolder(); 1029 mSelectedVH.allDay = 1030 cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0; 1031 tempCursor = cursor; 1032 } 1033 } else if (newPosition != -1) { 1034 tempCursor = getCursorByPosition(newPosition); 1035 tempCursorPosition = getCursorPositionByPosition(newPosition); 1036 } 1037 if (tempCursor != null) { 1038 EventInfo event = buildEventInfoFromCursor(tempCursor, tempCursorPosition, 1039 false); 1040 long selectedTime = findStartTimeFromPosition(newPosition); 1041 CalendarController.getInstance(mContext).sendEventRelatedEventWithExtra( 1042 this, EventType.VIEW_EVENT, event.id, event.begin, 1043 event.end, 0, 0, CalendarController.EventInfo.buildViewExtraLong( 1044 Attendees.ATTENDEE_STATUS_NONE, event.allDay), 1045 selectedTime); 1046 } 1047 } 1048 } else { 1049 cursor.close(); 1050 } 1051 1052 // Update header and footer 1053 if (!mDoneSettingUpHeaderFooter) { 1054 OnClickListener headerFooterOnClickListener = new OnClickListener() { 1055 public void onClick(View v) { 1056 if (v == mHeaderView) { 1057 queueQuery(new QuerySpec(QUERY_TYPE_OLDER)); 1058 } else { 1059 queueQuery(new QuerySpec(QUERY_TYPE_NEWER)); 1060 } 1061 }}; 1062 mHeaderView.setOnClickListener(headerFooterOnClickListener); 1063 mFooterView.setOnClickListener(headerFooterOnClickListener); 1064 mAgendaListView.addFooterView(mFooterView); 1065 mDoneSettingUpHeaderFooter = true; 1066 } 1067 synchronized (mQueryQueue) { 1068 int totalAgendaRangeStart = -1; 1069 int totalAgendaRangeEnd = -1; 1070 1071 if (cursorSize != 0) { 1072 // Remove the query that just completed 1073 QuerySpec x = mQueryQueue.poll(); 1074 if (BASICLOG && !x.equals(data)) { 1075 Log.e(TAG, "onQueryComplete - cookie != head of queue"); 1076 } 1077 mEmptyCursorCount = 0; 1078 if (data.queryType == QUERY_TYPE_NEWER) { 1079 mNewerRequestsProcessed++; 1080 } else if (data.queryType == QUERY_TYPE_OLDER) { 1081 mOlderRequestsProcessed++; 1082 } 1083 1084 totalAgendaRangeStart = mAdapterInfos.getFirst().start; 1085 totalAgendaRangeEnd = mAdapterInfos.getLast().end; 1086 } else { // CursorSize == 0 1087 QuerySpec querySpec = mQueryQueue.peek(); 1088 1089 // Update Adapter Info with new start and end date range 1090 if (!mAdapterInfos.isEmpty()) { 1091 DayAdapterInfo first = mAdapterInfos.getFirst(); 1092 DayAdapterInfo last = mAdapterInfos.getLast(); 1093 1094 if (first.start - 1 <= querySpec.end && querySpec.start < first.start) { 1095 first.start = querySpec.start; 1096 } 1097 1098 if (querySpec.start <= last.end + 1 && last.end < querySpec.end) { 1099 last.end = querySpec.end; 1100 } 1101 1102 totalAgendaRangeStart = first.start; 1103 totalAgendaRangeEnd = last.end; 1104 } else { 1105 totalAgendaRangeStart = querySpec.start; 1106 totalAgendaRangeEnd = querySpec.end; 1107 } 1108 1109 // Update query specification with expanded search range 1110 // and maybe rerun query 1111 switch (querySpec.queryType) { 1112 case QUERY_TYPE_OLDER: 1113 totalAgendaRangeStart = querySpec.start; 1114 querySpec.start -= MAX_QUERY_DURATION; 1115 break; 1116 case QUERY_TYPE_NEWER: 1117 totalAgendaRangeEnd = querySpec.end; 1118 querySpec.end += MAX_QUERY_DURATION; 1119 break; 1120 case QUERY_TYPE_CLEAN: 1121 totalAgendaRangeStart = querySpec.start; 1122 totalAgendaRangeEnd = querySpec.end; 1123 querySpec.start -= MAX_QUERY_DURATION / 2; 1124 querySpec.end += MAX_QUERY_DURATION / 2; 1125 break; 1126 } 1127 1128 if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) { 1129 // Nothing in the cursor again. Dropping query 1130 mQueryQueue.poll(); 1131 } 1132 } 1133 1134 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd); 1135 1136 // Go over the events and mark the first day after yesterday 1137 // that has events in it 1138 // If the range of adapters doesn't include yesterday, skip marking it since it will 1139 // mark the first day in the adapters. 1140 synchronized (mAdapterInfos) { 1141 DayAdapterInfo info = mAdapterInfos.getFirst(); 1142 Time time = new Time(mTimeZone); 1143 long now = System.currentTimeMillis(); 1144 time.set(now); 1145 int JulianToday = Time.getJulianDay(now, time.gmtoff); 1146 if (info != null && JulianToday >= info.start && JulianToday 1147 <= mAdapterInfos.getLast().end) { 1148 Iterator<DayAdapterInfo> iter = mAdapterInfos.iterator(); 1149 boolean foundDay = false; 1150 while (iter.hasNext() && !foundDay) { 1151 info = iter.next(); 1152 for (int i = 0; i < info.size; i++) { 1153 if (info.dayAdapter.findJulianDayFromPosition(i) >= JulianToday) { 1154 info.dayAdapter.setAsFirstDayAfterYesterday(i); 1155 foundDay = true; 1156 break; 1157 } 1158 } 1159 } 1160 } 1161 } 1162 1163 // Fire off the next query if any 1164 Iterator<QuerySpec> it = mQueryQueue.iterator(); 1165 while (it.hasNext()) { 1166 QuerySpec queryData = it.next(); 1167 if (queryData.queryType == QUERY_TYPE_CLEAN 1168 || !isInRange(queryData.start, queryData.end)) { 1169 // Query accepted 1170 if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size()); 1171 doQuery(queryData); 1172 break; 1173 } else { 1174 // Query rejected 1175 it.remove(); 1176 if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size()); 1177 } 1178 } 1179 } 1180 if (BASICLOG) { 1181 for (DayAdapterInfo info3 : mAdapterInfos) { 1182 Log.e(TAG, "> " + info3.toString()); 1183 } 1184 } 1185 } 1186 1187 /* 1188 * Update the adapter info array with a the new cursor. Close out old 1189 * cursors as needed. 1190 * 1191 * @return number of rows removed from the beginning 1192 */ 1193 private int processNewCursor(QuerySpec data, Cursor cursor) { 1194 synchronized (mAdapterInfos) { 1195 // Remove adapter info's from adapterInfos as needed 1196 DayAdapterInfo info = pruneAdapterInfo(data.queryType); 1197 int listPositionOffset = 0; 1198 if (info == null) { 1199 info = new DayAdapterInfo(mContext); 1200 } else { 1201 if (DEBUGLOG) 1202 Log.e(TAG, "processNewCursor listPositionOffsetA=" 1203 + -info.size); 1204 listPositionOffset = -info.size; 1205 } 1206 1207 // Setup adapter info 1208 info.start = data.start; 1209 info.end = data.end; 1210 info.cursor = cursor; 1211 info.dayAdapter.changeCursor(info); 1212 info.size = info.dayAdapter.getCount(); 1213 1214 // Insert into adapterInfos 1215 if (mAdapterInfos.isEmpty() 1216 || data.end <= mAdapterInfos.getFirst().start) { 1217 mAdapterInfos.addFirst(info); 1218 listPositionOffset += info.size; 1219 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) { 1220 mAdapterInfos.addLast(info); 1221 for (DayAdapterInfo info2 : mAdapterInfos) { 1222 Log.e("========== BUG ==", info2.toString()); 1223 } 1224 } else { 1225 mAdapterInfos.addLast(info); 1226 } 1227 1228 // Update offsets in adapterInfos 1229 mRowCount = 0; 1230 for (DayAdapterInfo info3 : mAdapterInfos) { 1231 info3.offset = mRowCount; 1232 mRowCount += info3.size; 1233 } 1234 mLastUsedInfo = null; 1235 1236 return listPositionOffset; 1237 } 1238 } 1239 } 1240 1241 static String getViewTitle(View x) { 1242 String title = ""; 1243 if (x != null) { 1244 Object yy = x.getTag(); 1245 if (yy instanceof AgendaAdapter.ViewHolder) { 1246 TextView tv = ((AgendaAdapter.ViewHolder) yy).title; 1247 if (tv != null) { 1248 title = (String) tv.getText(); 1249 } 1250 } else if (yy != null) { 1251 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView; 1252 if (dateView != null) { 1253 title = (String) dateView.getText(); 1254 } 1255 } 1256 } 1257 return title; 1258 } 1259 1260 public void onResume() { 1261 mTZUpdater.run(); 1262 } 1263 1264 public void setHideDeclinedEvents(boolean hideDeclined) { 1265 mHideDeclined = hideDeclined; 1266 } 1267 1268 public void setSelectedView(View v) { 1269 if (v != null) { 1270 Object vh = v.getTag(); 1271 if (vh instanceof AgendaAdapter.ViewHolder) { 1272 mSelectedVH = (AgendaAdapter.ViewHolder) vh; 1273 if (mSelectedInstanceId != mSelectedVH.instanceId) { 1274 mSelectedInstanceId = mSelectedVH.instanceId; 1275 notifyDataSetChanged(); 1276 } 1277 } 1278 } 1279 } 1280 1281 public AgendaAdapter.ViewHolder getSelectedViewHolder() { 1282 return mSelectedVH; 1283 } 1284 1285 public long getSelectedInstanceId() { 1286 return mSelectedInstanceId; 1287 } 1288 1289 public void setSelectedInstanceId(long selectedInstanceId) { 1290 mSelectedInstanceId = selectedInstanceId; 1291 mSelectedVH = null; 1292 } 1293 1294 private long findInstanceIdFromPosition(int position) { 1295 DayAdapterInfo info = getAdapterInfoByPosition(position); 1296 if (info != null) { 1297 return info.dayAdapter.getInstanceId(position - info.offset); 1298 } 1299 return -1; 1300 } 1301 1302 private long findStartTimeFromPosition(int position) { 1303 DayAdapterInfo info = getAdapterInfoByPosition(position); 1304 if (info != null) { 1305 return info.dayAdapter.getStartTime(position - info.offset); 1306 } 1307 return -1; 1308 } 1309 1310 1311 private Cursor getCursorByPosition(int position) { 1312 DayAdapterInfo info = getAdapterInfoByPosition(position); 1313 if (info != null) { 1314 return info.cursor; 1315 } 1316 return null; 1317 } 1318 1319 private int getCursorPositionByPosition(int position) { 1320 DayAdapterInfo info = getAdapterInfoByPosition(position); 1321 if (info != null) { 1322 return info.dayAdapter.getCursorPosition(position - info.offset); 1323 } 1324 return -1; 1325 } 1326 1327 // Implementation of HeaderIndexer interface for StickyHeeaderListView 1328 1329 // Returns the location of the day header of a specific event specified in the position 1330 // in the adapter 1331 @Override 1332 public int getHeaderPositionFromItemPosition(int position) { 1333 1334 // For phone configuration, return -1 so there will be no sticky header 1335 if (!mIsTabletConfig) { 1336 return -1; 1337 } 1338 1339 DayAdapterInfo info = getAdapterInfoByPosition(position); 1340 if (info != null) { 1341 int pos = info.dayAdapter.getHeaderPosition(position - info.offset); 1342 return (pos != -1)?(pos + info.offset):-1; 1343 } 1344 return -1; 1345 } 1346 1347 // Returns the number of events for a specific day header 1348 @Override 1349 public int getHeaderItemsNumber(int headerPosition) { 1350 if (headerPosition < 0 || !mIsTabletConfig) { 1351 return -1; 1352 } 1353 DayAdapterInfo info = getAdapterInfoByPosition(headerPosition); 1354 if (info != null) { 1355 return info.dayAdapter.getHeaderItemsCount(headerPosition - info.offset); 1356 } 1357 return -1; 1358 } 1359 1360 @Override 1361 public void OnHeaderHeightChanged(int height) { 1362 mStickyHeaderSize = height; 1363 } 1364 1365 public int getStickyHeaderHeight() { 1366 return mStickyHeaderSize; 1367 } 1368 1369 public void setScrollState(int state) { 1370 mListViewScrollState = state; 1371 } 1372 } 1373