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