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