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