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 
QuerySpec(int queryType)245         public QuerySpec(int queryType) {
246             this.queryType = queryType;
247             id = -1;
248         }
249 
250         @Override
hashCode()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
equals(Object obj)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 
DayAdapterInfo(Context context)317         public DayAdapterInfo(Context context) {
318             dayAdapter = new AgendaByDayAdapter(context);
319         }
320 
321         @Override
toString()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 
AgendaWindowAdapter(Context context, AgendaListView agendaListView, boolean showEventOnStart)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
getViewTypeCount()374     public int getViewTypeCount() {
375         return AgendaByDayAdapter.TYPE_LAST;
376     }
377 
378     // Method in BaseAdapter
379     @Override
areAllItemsEnabled()380     public boolean areAllItemsEnabled() {
381         return false;
382     }
383 
384     // Method in Adapter
385     @Override
getItemViewType(int position)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
isEnabled(int position)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
getCount()407     public int getCount() {
408         return mRowCount;
409     }
410 
411     // Abstract Method in BaseAdapter
getItem(int position)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
hasStableIds()423     public boolean hasStableIds() {
424         return true;
425     }
426 
427     // Abstract Method in BaseAdapter
428     @Override
getItemId(int position)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
getView(int position, View convertView, ViewGroup parent)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 
findEventPositionNearestTime(Time time, long id)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 
getAdapterInfoByPosition(int position)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 
getAdapterInfoByTime(Time time)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 
getAgendaItemByPosition(final int positionInListView)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      */
getAgendaItemByPosition(final int positionInListView, boolean returnEventStartDay)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 
buildAgendaItemFromCursor(final Cursor cursor, int cursorPosition, boolean isDayHeader)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      */
sendViewEvent(AgendaItem item, long selectedTime)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 
refresh(Time goToTime, long id, String searchQuery, boolean forced, boolean refreshEventInfo)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 
close()756     public void close() {
757         mShuttingDown = true;
758         pruneAdapterInfo(QUERY_TYPE_CLEAN);
759         if (mQueryHandler != null) {
760             mQueryHandler.cancelOperation(0);
761         }
762     }
763 
pruneAdapterInfo(int queryType)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 
buildQuerySelection()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 
buildQueryUri(int start, int end, String searchQuery)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 
isInRange(int start, int end)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 
calculateQueryDuration(int start, int end)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 
queueQuery(int start, int end, Time goToTime, String searchQuery, int queryType, long id)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 
queueQuery(QuerySpec queryData)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 
doQuery(QuerySpec queryData)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 
formatDateString(int julianDay)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 
updateHeaderFooter(final int start, final int end)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 
QueryHandler(ContentResolver cr)959         public QueryHandler(ContentResolver cr) {
960             super(cr);
961         }
962 
963         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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          */
processNewCursor(QuerySpec data, Cursor cursor)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 
getViewTitle(View x)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 
onResume()1300     public void onResume() {
1301         mTZUpdater.run();
1302     }
1303 
setHideDeclinedEvents(boolean hideDeclined)1304     public void setHideDeclinedEvents(boolean hideDeclined) {
1305         mHideDeclined = hideDeclined;
1306     }
1307 
setSelectedView(View v)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 
getSelectedViewHolder()1321     public AgendaAdapter.ViewHolder getSelectedViewHolder() {
1322         return mSelectedVH;
1323     }
1324 
getSelectedInstanceId()1325     public long getSelectedInstanceId() {
1326         return mSelectedInstanceId;
1327     }
1328 
setSelectedInstanceId(long selectedInstanceId)1329     public void setSelectedInstanceId(long selectedInstanceId) {
1330         mSelectedInstanceId = selectedInstanceId;
1331         mSelectedVH = null;
1332     }
1333 
findInstanceIdFromPosition(int position)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 
findStartTimeFromPosition(int position)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 
getCursorByPosition(int position)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 
getCursorPositionByPosition(int position)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
getHeaderPositionFromItemPosition(int position)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
getHeaderItemsNumber(int headerPosition)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
OnHeaderHeightChanged(int height)1401     public void OnHeaderHeightChanged(int height) {
1402         mStickyHeaderSize = height;
1403     }
1404 
getStickyHeaderHeight()1405     public int getStickyHeaderHeight() {
1406         return mStickyHeaderSize;
1407     }
1408 
setScrollState(int state)1409     public void setScrollState(int state) {
1410         mListViewScrollState = state;
1411     }
1412 }
1413