• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import android.content.AsyncQueryHandler;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.provider.Calendar.Attendees;
25 import android.provider.Calendar.Calendars;
26 import android.provider.Calendar.Instances;
27 import android.text.format.DateUtils;
28 import android.text.format.Time;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.View.OnClickListener;
33 import android.view.ViewGroup;
34 import android.widget.BaseAdapter;
35 import android.widget.TextView;
36 
37 import java.util.Iterator;
38 import java.util.LinkedList;
39 import java.util.concurrent.ConcurrentLinkedQueue;
40 
41 /*
42 Bugs Bugs Bugs:
43 - At rotation and launch time, the initial position is not set properly. This code is calling
44  listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
45 - Scroll using trackball isn't repositioning properly after a new adapter is added.
46 - Track ball clicks at the header/footer doesn't work.
47 - Potential ping pong effect if the prefetch window is big and data is limited
48 - Add index in calendar provider
49 
50 ToDo ToDo ToDo:
51 Get design of header and footer from designer
52 
53 Make scrolling smoother.
54 Test for correctness
55 Loading speed
56 Check for leaks and excessive allocations
57  */
58 
59 public class AgendaWindowAdapter extends BaseAdapter {
60 
61     static final boolean BASICLOG = false;
62     static final boolean DEBUGLOG = false;
63     private static String TAG = "AgendaWindowAdapter";
64 
65     private static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
66     public static final int INDEX_TITLE = 1;
67     public static final int INDEX_EVENT_LOCATION = 2;
68     public static final int INDEX_ALL_DAY = 3;
69     public static final int INDEX_HAS_ALARM = 4;
70     public static final int INDEX_COLOR = 5;
71     public static final int INDEX_RRULE = 6;
72     public static final int INDEX_BEGIN = 7;
73     public static final int INDEX_END = 8;
74     public static final int INDEX_EVENT_ID = 9;
75     public static final int INDEX_START_DAY = 10;
76     public static final int INDEX_END_DAY = 11;
77     public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
78 
79     private static final String[] PROJECTION = new String[] {
80             Instances._ID, // 0
81             Instances.TITLE, // 1
82             Instances.EVENT_LOCATION, // 2
83             Instances.ALL_DAY, // 3
84             Instances.HAS_ALARM, // 4
85             Instances.COLOR, // 5
86             Instances.RRULE, // 6
87             Instances.BEGIN, // 7
88             Instances.END, // 8
89             Instances.EVENT_ID, // 9
90             Instances.START_DAY, // 10 Julian start day
91             Instances.END_DAY, // 11 Julian end day
92             Instances.SELF_ATTENDEE_STATUS, // 12
93     };
94 
95     // Listview may have a bug where the index/position is not consistent when there's a header.
96     // TODO Need to look into this.
97     private static final int OFF_BY_ONE_BUG = 1;
98 
99     private static final int MAX_NUM_OF_ADAPTERS = 5;
100 
101     private static final int IDEAL_NUM_OF_EVENTS = 50;
102 
103     private static final int MIN_QUERY_DURATION = 7; // days
104 
105     private static final int MAX_QUERY_DURATION = 60; // days
106 
107     private static final int PREFETCH_BOUNDARY = 1;
108 
109     // Times to auto-expand/retry query after getting no data
110     private static final int RETRIES_ON_NO_DATA = 0;
111 
112     private Context mContext;
113 
114     private QueryHandler mQueryHandler;
115 
116     private AgendaListView mAgendaListView;
117 
118     private int mRowCount; // The sum of the rows in all the adapters
119 
120     private int mEmptyCursorCount;
121 
122     private DayAdapterInfo mLastUsedInfo; // Cached value of the last used adapter.
123 
124     private LinkedList<DayAdapterInfo> mAdapterInfos = new LinkedList<DayAdapterInfo>();
125 
126     private ConcurrentLinkedQueue<QuerySpec> mQueryQueue = new ConcurrentLinkedQueue<QuerySpec>();
127 
128     private TextView mHeaderView;
129 
130     private TextView mFooterView;
131 
132     private boolean mDoneSettingUpHeaderFooter = false;
133 
134     /*
135      * When the user scrolled to the top, a query will be made for older events
136      * and this will be incremented. Don't make more requests if
137      * mOlderRequests > mOlderRequestsProcessed.
138      */
139     private int mOlderRequests;
140 
141     // Number of "older" query that has been processed.
142     private int mOlderRequestsProcessed;
143 
144     /*
145      * When the user scrolled to the bottom, a query will be made for newer
146      * events and this will be incremented. Don't make more requests if
147      * mNewerRequests > mNewerRequestsProcessed.
148      */
149     private int mNewerRequests;
150 
151     // Number of "newer" query that has been processed.
152     private int mNewerRequestsProcessed;
153 
154     private boolean mShuttingDown;
155     private boolean mHideDeclined;
156 
157     // Types of Query
158     private static final int QUERY_TYPE_OLDER = 0; // Query for older events
159     private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
160     private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
161 
162     // Placeholder if we need some code for updating the tz later.
163     private Runnable mUpdateTZ = null;
164 
165     private static class QuerySpec {
166         long queryStartMillis;
167 
168         Time goToTime;
169 
170         int start;
171 
172         int end;
173 
174         int queryType;
175 
QuerySpec(int queryType)176         public QuerySpec(int queryType) {
177             this.queryType = queryType;
178         }
179 
180         @Override
hashCode()181         public int hashCode() {
182             final int prime = 31;
183             int result = 1;
184             result = prime * result + end;
185             result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
186             result = prime * result + queryType;
187             result = prime * result + start;
188             if (goToTime != null) {
189                 long goToTimeMillis = goToTime.toMillis(false);
190                 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
191             }
192             return result;
193         }
194 
195         @Override
equals(Object obj)196         public boolean equals(Object obj) {
197             if (this == obj) return true;
198             if (obj == null) return false;
199             if (getClass() != obj.getClass()) return false;
200             QuerySpec other = (QuerySpec) obj;
201             if (end != other.end || queryStartMillis != other.queryStartMillis
202                     || queryType != other.queryType || start != other.start) {
203                 return false;
204             }
205             if (goToTime != null) {
206                 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
207                     return false;
208                 }
209             } else {
210                 if (other.goToTime != null) {
211                     return false;
212                 }
213             }
214             return true;
215         }
216     }
217 
218     static class EventInfo {
219         long begin;
220 
221         long end;
222 
223         long id;
224 
225         boolean allday;
226     }
227 
228     static class DayAdapterInfo {
229         Cursor cursor;
230 
231         AgendaByDayAdapter dayAdapter;
232 
233         int start; // start day of the cursor's coverage
234 
235         int end; // end day of the cursor's coverage
236 
237         int offset; // offset in position in the list view
238 
239         int size; // dayAdapter.getCount()
240 
DayAdapterInfo(Context context)241         public DayAdapterInfo(Context context) {
242             dayAdapter = new AgendaByDayAdapter(context);
243         }
244 
245         @Override
toString()246         public String toString() {
247             Time time = new Time();
248             StringBuilder sb = new StringBuilder();
249             time.setJulianDay(start);
250             time.normalize(false);
251             sb.append("Start:").append(time.toString());
252             time.setJulianDay(end);
253             time.normalize(false);
254             sb.append(" End:").append(time.toString());
255             sb.append(" Offset:").append(offset);
256             sb.append(" Size:").append(size);
257             return sb.toString();
258         }
259     }
260 
AgendaWindowAdapter(AgendaActivity agendaActivity, AgendaListView agendaListView)261     public AgendaWindowAdapter(AgendaActivity agendaActivity,
262             AgendaListView agendaListView) {
263         mContext = agendaActivity;
264         mAgendaListView = agendaListView;
265         mQueryHandler = new QueryHandler(agendaActivity.getContentResolver());
266 
267         LayoutInflater inflater = (LayoutInflater) agendaActivity
268                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
269         mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
270         mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
271         mHeaderView.setText(R.string.loading);
272         mAgendaListView.addHeaderView(mHeaderView);
273     }
274 
275     // Method in Adapter
276     @Override
getViewTypeCount()277     public int getViewTypeCount() {
278         return AgendaByDayAdapter.TYPE_LAST;
279     }
280 
281     // Method in BaseAdapter
282     @Override
areAllItemsEnabled()283     public boolean areAllItemsEnabled() {
284         return false;
285     }
286 
287     // Method in Adapter
288     @Override
getItemViewType(int position)289     public int getItemViewType(int position) {
290         DayAdapterInfo info = getAdapterInfoByPosition(position);
291         if (info != null) {
292             return info.dayAdapter.getItemViewType(position - info.offset);
293         } else {
294             return -1;
295         }
296     }
297 
298     // Method in BaseAdapter
299     @Override
isEnabled(int position)300     public boolean isEnabled(int position) {
301         DayAdapterInfo info = getAdapterInfoByPosition(position);
302         if (info != null) {
303             return info.dayAdapter.isEnabled(position - info.offset);
304         } else {
305             return false;
306         }
307     }
308 
309     // Abstract Method in BaseAdapter
getCount()310     public int getCount() {
311         return mRowCount;
312     }
313 
314     // Abstract Method in BaseAdapter
getItem(int position)315     public Object getItem(int position) {
316         DayAdapterInfo info = getAdapterInfoByPosition(position);
317         if (info != null) {
318             return info.dayAdapter.getItem(position - info.offset);
319         } else {
320             return null;
321         }
322     }
323 
324     // Method in BaseAdapter
325     @Override
hasStableIds()326     public boolean hasStableIds() {
327         return true;
328     }
329 
330     // Abstract Method in BaseAdapter
getItemId(int position)331     public long getItemId(int position) {
332         DayAdapterInfo info = getAdapterInfoByPosition(position);
333         if (info != null) {
334             return ((position - info.offset) << 20) + info.start ;
335         } else {
336             return -1;
337         }
338     }
339 
340     // Abstract Method in BaseAdapter
getView(int position, View convertView, ViewGroup parent)341     public View getView(int position, View convertView, ViewGroup parent) {
342         if (position >= (mRowCount - PREFETCH_BOUNDARY)
343                 && mNewerRequests <= mNewerRequestsProcessed) {
344             if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
345             mNewerRequests++;
346             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
347         }
348 
349         if (position < PREFETCH_BOUNDARY
350                 && mOlderRequests <= mOlderRequestsProcessed) {
351             if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
352             mOlderRequests++;
353             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
354         }
355 
356         View v;
357         DayAdapterInfo info = getAdapterInfoByPosition(position);
358         if (info != null) {
359             v = info.dayAdapter.getView(position - info.offset, convertView,
360                     parent);
361         } else {
362             //TODO
363             Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
364             TextView tv = new TextView(mContext);
365             tv.setText("Bug! " + position);
366             v = tv;
367         }
368 
369         if (DEBUGLOG) {
370             Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
371         }
372         return v;
373     }
374 
findDayPositionNearestTime(Time time)375     private int findDayPositionNearestTime(Time time) {
376         if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time);
377 
378         DayAdapterInfo info = getAdapterInfoByTime(time);
379         if (info != null) {
380             return info.offset + info.dayAdapter.findDayPositionNearestTime(time);
381         } else {
382             return -1;
383         }
384     }
385 
getAdapterInfoByPosition(int position)386     private DayAdapterInfo getAdapterInfoByPosition(int position) {
387         synchronized (mAdapterInfos) {
388             if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
389                     && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
390                 return mLastUsedInfo;
391             }
392             for (DayAdapterInfo info : mAdapterInfos) {
393                 if (info.offset <= position
394                         && position < (info.offset + info.size)) {
395                     mLastUsedInfo = info;
396                     return info;
397                 }
398             }
399         }
400         return null;
401     }
402 
getAdapterInfoByTime(Time time)403     private DayAdapterInfo getAdapterInfoByTime(Time time) {
404         if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
405 
406         Time tmpTime = new Time(time);
407         long timeInMillis = tmpTime.normalize(true);
408         int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
409         synchronized (mAdapterInfos) {
410             for (DayAdapterInfo info : mAdapterInfos) {
411                 if (info.start <= day && day < info.end) {
412                     return info;
413                 }
414             }
415         }
416         return null;
417     }
418 
getEventByPosition(int position)419     public EventInfo getEventByPosition(int position) {
420         if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + position);
421 
422         EventInfo event = new EventInfo();
423         position -= OFF_BY_ONE_BUG;
424         DayAdapterInfo info = getAdapterInfoByPosition(position);
425         if (info == null) {
426             return null;
427         }
428 
429         position = info.dayAdapter.getCursorPosition(position - info.offset);
430         if (position == Integer.MIN_VALUE) {
431             return null;
432         }
433 
434         boolean isDayHeader = false;
435         if (position < 0) {
436             position = -position;
437             isDayHeader = true;
438         }
439 
440         if (position < info.cursor.getCount()) {
441             info.cursor.moveToPosition(position);
442             event.begin = info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
443             boolean allDay = info.cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
444 
445             event.allday = allDay;
446             if (!isDayHeader) {
447                 event.end = info.cursor.getLong(AgendaWindowAdapter.INDEX_END);
448                 event.id = info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
449             }
450             if (allDay) { // UTC
451                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
452                 time.setJulianDay(Time.getJulianDay(event.begin, 0));
453                 event.begin = time.toMillis(true /* use isDst */);
454                 if (!isDayHeader) {
455                     time.setJulianDay(Time.getJulianDay(event.end, 0));
456                     event.end = time.toMillis(true);
457                 }
458             } else if (isDayHeader) { // Trim to midnight.
459                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
460                 time.set(event.begin);
461                 time.hour = 0;
462                 time.minute = 0;
463                 time.second = 0;
464                 event.begin = time.toMillis(false /* use isDst */);
465             }
466 
467             return event;
468         }
469         return null;
470     }
471 
refresh(Time goToTime, boolean forced)472     public void refresh(Time goToTime, boolean forced) {
473         if (DEBUGLOG) {
474             Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced"));
475         }
476 
477         int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
478 
479         if (!forced && isInRange(startDay, startDay)) {
480             // No need to requery
481             mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG);
482             return;
483         }
484 
485         // Query for a total of MIN_QUERY_DURATION days
486         int endDay = startDay + MIN_QUERY_DURATION;
487 
488         queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN);
489     }
490 
close()491     public void close() {
492         mShuttingDown = true;
493         pruneAdapterInfo(QUERY_TYPE_CLEAN);
494         if (mQueryHandler != null) {
495             mQueryHandler.cancelOperation(0);
496         }
497     }
498 
pruneAdapterInfo(int queryType)499     private DayAdapterInfo pruneAdapterInfo(int queryType) {
500         synchronized (mAdapterInfos) {
501             DayAdapterInfo recycleMe = null;
502             if (!mAdapterInfos.isEmpty()) {
503                 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
504                     if (queryType == QUERY_TYPE_NEWER) {
505                         recycleMe = mAdapterInfos.removeFirst();
506                     } else if (queryType == QUERY_TYPE_OLDER) {
507                         recycleMe = mAdapterInfos.removeLast();
508                         // Keep the size only if the oldest items are removed.
509                         recycleMe.size = 0;
510                     }
511                     if (recycleMe != null) {
512                         if (recycleMe.cursor != null) {
513                             recycleMe.cursor.close();
514                         }
515                         return recycleMe;
516                     }
517                 }
518 
519                 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
520                     mRowCount = 0;
521                     int deletedRows = 0;
522                     DayAdapterInfo info;
523                     do {
524                         info = mAdapterInfos.poll();
525                         if (info != null) {
526                             info.cursor.close();
527                             deletedRows += info.size;
528                             recycleMe = info;
529                         }
530                     } while (info != null);
531 
532                     if (recycleMe != null) {
533                         recycleMe.cursor = null;
534                         recycleMe.size = deletedRows;
535                     }
536                 }
537             }
538             return recycleMe;
539         }
540     }
541 
buildQuerySelection()542     private String buildQuerySelection() {
543         // Respect the preference to show/hide declined events
544 
545         if (mHideDeclined) {
546             return Calendars.SELECTED + "=1 AND "
547                     + Instances.SELF_ATTENDEE_STATUS + "!="
548                     + Attendees.ATTENDEE_STATUS_DECLINED;
549         } else {
550             return Calendars.SELECTED + "=1";
551         }
552     }
553 
buildQueryUri(int start, int end)554     private Uri buildQueryUri(int start, int end) {
555         StringBuilder path = new StringBuilder();
556         path.append(start);
557         path.append('/');
558         path.append(end);
559         Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString());
560         return uri;
561     }
562 
isInRange(int start, int end)563     private boolean isInRange(int start, int end) {
564         synchronized (mAdapterInfos) {
565             if (mAdapterInfos.isEmpty()) {
566                 return false;
567             }
568             return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
569         }
570     }
571 
calculateQueryDuration(int start, int end)572     private int calculateQueryDuration(int start, int end) {
573         int queryDuration = MAX_QUERY_DURATION;
574         if (mRowCount != 0) {
575             queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
576         }
577 
578         if (queryDuration > MAX_QUERY_DURATION) {
579             queryDuration = MAX_QUERY_DURATION;
580         } else if (queryDuration < MIN_QUERY_DURATION) {
581             queryDuration = MIN_QUERY_DURATION;
582         }
583 
584         return queryDuration;
585     }
586 
queueQuery(int start, int end, Time goToTime, int queryType)587     private boolean queueQuery(int start, int end, Time goToTime, int queryType) {
588         QuerySpec queryData = new QuerySpec(queryType);
589         queryData.goToTime = goToTime;
590         queryData.start = start;
591         queryData.end = end;
592         return queueQuery(queryData);
593     }
594 
queueQuery(QuerySpec queryData)595     private boolean queueQuery(QuerySpec queryData) {
596         Boolean queuedQuery;
597         synchronized (mQueryQueue) {
598             queuedQuery = false;
599             Boolean doQueryNow = mQueryQueue.isEmpty();
600             mQueryQueue.add(queryData);
601             queuedQuery = true;
602             if (doQueryNow) {
603                 doQuery(queryData);
604             }
605         }
606         return queuedQuery;
607     }
608 
doQuery(QuerySpec queryData)609     private void doQuery(QuerySpec queryData) {
610         if (!mAdapterInfos.isEmpty()) {
611             int start = mAdapterInfos.getFirst().start;
612             int end = mAdapterInfos.getLast().end;
613             int queryDuration = calculateQueryDuration(start, end);
614             switch(queryData.queryType) {
615                 case QUERY_TYPE_OLDER:
616                     queryData.end = start - 1;
617                     queryData.start = queryData.end - queryDuration;
618                     break;
619                 case QUERY_TYPE_NEWER:
620                     queryData.start = end + 1;
621                     queryData.end = queryData.start + queryDuration;
622                     break;
623             }
624         }
625 
626         if (BASICLOG) {
627             Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
628             time.setJulianDay(queryData.start);
629             Time time2 = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
630             time2.setJulianDay(queryData.end);
631             Log.v(TAG, "startQuery: " + time.toString() + " to "
632                     + time2.toString() + " then go to " + queryData.goToTime);
633         }
634 
635         mQueryHandler.cancelOperation(0);
636         if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
637         mQueryHandler.startQuery(0, queryData, buildQueryUri(
638                 queryData.start, queryData.end), PROJECTION,
639                 buildQuerySelection(), null, AGENDA_SORT_ORDER);
640     }
641 
formatDateString(int julianDay)642     private String formatDateString(int julianDay) {
643         Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
644         time.setJulianDay(julianDay);
645         long millis = time.toMillis(false);
646         return Utils.formatDateRange(mContext, millis, millis,
647                 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
648                         | DateUtils.FORMAT_ABBREV_MONTH).toString();
649     }
650 
updateHeaderFooter(final int start, final int end)651     private void updateHeaderFooter(final int start, final int end) {
652         mHeaderView.setText(mContext.getString(R.string.show_older_events,
653                 formatDateString(start)));
654         mFooterView.setText(mContext.getString(R.string.show_newer_events,
655                 formatDateString(end)));
656     }
657 
658     private class QueryHandler extends AsyncQueryHandler {
659 
QueryHandler(ContentResolver cr)660         public QueryHandler(ContentResolver cr) {
661             super(cr);
662         }
663 
664         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)665         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
666             QuerySpec data = (QuerySpec)cookie;
667             if (BASICLOG) {
668                 long queryEndMillis = System.nanoTime();
669                 Log.e(TAG, "Query time(ms): "
670                         + (queryEndMillis - data.queryStartMillis) / 1000000
671                         + " Count: " + cursor.getCount());
672             }
673 
674             if (mShuttingDown) {
675                 cursor.close();
676                 return;
677             }
678 
679             // Notify Listview of changes and update position
680             int cursorSize = cursor.getCount();
681             if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
682                 final int listPositionOffset = processNewCursor(data, cursor);
683                 if (data.goToTime == null) { // Typical Scrolling type query
684                     notifyDataSetChanged();
685                     if (listPositionOffset != 0) {
686                         mAgendaListView.shiftSelection(listPositionOffset);
687                     }
688                 } else { // refresh() called. Go to the designated position
689                     final Time goToTime = data.goToTime;
690                     notifyDataSetChanged();
691                     int newPosition = findDayPositionNearestTime(goToTime);
692                     if (newPosition >= 0) {
693                         mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG);
694                     }
695                     if (DEBUGLOG) {
696                         Log.e(TAG, "Setting listview to " +
697                                 "findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
698                     }
699                 }
700             } else {
701                 cursor.close();
702             }
703 
704             // Update header and footer
705             if (!mDoneSettingUpHeaderFooter) {
706                 OnClickListener headerFooterOnClickListener = new OnClickListener() {
707                     public void onClick(View v) {
708                         if (v == mHeaderView) {
709                             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
710                         } else {
711                             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
712                         }
713                     }};
714                 mHeaderView.setOnClickListener(headerFooterOnClickListener);
715                 mFooterView.setOnClickListener(headerFooterOnClickListener);
716                 mAgendaListView.addFooterView(mFooterView);
717                 mDoneSettingUpHeaderFooter = true;
718             }
719             synchronized (mQueryQueue) {
720                 int totalAgendaRangeStart = -1;
721                 int totalAgendaRangeEnd = -1;
722 
723                 if (cursorSize != 0) {
724                     // Remove the query that just completed
725                     QuerySpec x = mQueryQueue.poll();
726                     if (BASICLOG && !x.equals(data)) {
727                         Log.e(TAG, "onQueryComplete - cookie != head of queue");
728                     }
729                     mEmptyCursorCount = 0;
730                     if (data.queryType == QUERY_TYPE_NEWER) {
731                         mNewerRequestsProcessed++;
732                     } else if (data.queryType == QUERY_TYPE_OLDER) {
733                         mOlderRequestsProcessed++;
734                     }
735 
736                     totalAgendaRangeStart = mAdapterInfos.getFirst().start;
737                     totalAgendaRangeEnd = mAdapterInfos.getLast().end;
738                 } else { // CursorSize == 0
739                     QuerySpec querySpec = mQueryQueue.peek();
740 
741                     // Update Adapter Info with new start and end date range
742                     if (!mAdapterInfos.isEmpty()) {
743                         DayAdapterInfo first = mAdapterInfos.getFirst();
744                         DayAdapterInfo last = mAdapterInfos.getLast();
745 
746                         if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
747                             first.start = querySpec.start;
748                         }
749 
750                         if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
751                             last.end = querySpec.end;
752                         }
753 
754                         totalAgendaRangeStart = first.start;
755                         totalAgendaRangeEnd = last.end;
756                     } else {
757                         totalAgendaRangeStart = querySpec.start;
758                         totalAgendaRangeEnd = querySpec.end;
759                     }
760 
761                     // Update query specification with expanded search range
762                     // and maybe rerun query
763                     switch (querySpec.queryType) {
764                         case QUERY_TYPE_OLDER:
765                             totalAgendaRangeStart = querySpec.start;
766                             querySpec.start -= MAX_QUERY_DURATION;
767                             break;
768                         case QUERY_TYPE_NEWER:
769                             totalAgendaRangeEnd = querySpec.end;
770                             querySpec.end += MAX_QUERY_DURATION;
771                             break;
772                         case QUERY_TYPE_CLEAN:
773                             totalAgendaRangeStart = querySpec.start;
774                             totalAgendaRangeEnd = querySpec.end;
775                             querySpec.start -= MAX_QUERY_DURATION / 2;
776                             querySpec.end += MAX_QUERY_DURATION / 2;
777                             break;
778                     }
779 
780                     if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
781                         // Nothing in the cursor again. Dropping query
782                         mQueryQueue.poll();
783                     }
784                 }
785 
786                 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
787 
788                 // Fire off the next query if any
789                 Iterator<QuerySpec> it = mQueryQueue.iterator();
790                 while (it.hasNext()) {
791                     QuerySpec queryData = it.next();
792                     if (!isInRange(queryData.start, queryData.end)) {
793                         // Query accepted
794                         if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
795                         doQuery(queryData);
796                         break;
797                     } else {
798                         // Query rejected
799                         it.remove();
800                         if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
801                     }
802                 }
803             }
804             if (BASICLOG) {
805                 for (DayAdapterInfo info3 : mAdapterInfos) {
806                     Log.e(TAG, "> " + info3.toString());
807                 }
808             }
809         }
810 
811         /*
812          * Update the adapter info array with a the new cursor. Close out old
813          * cursors as needed.
814          *
815          * @return number of rows removed from the beginning
816          */
processNewCursor(QuerySpec data, Cursor cursor)817         private int processNewCursor(QuerySpec data, Cursor cursor) {
818             synchronized (mAdapterInfos) {
819                 // Remove adapter info's from adapterInfos as needed
820                 DayAdapterInfo info = pruneAdapterInfo(data.queryType);
821                 int listPositionOffset = 0;
822                 if (info == null) {
823                     info = new DayAdapterInfo(mContext);
824                 } else {
825                     if (DEBUGLOG)
826                         Log.e(TAG, "processNewCursor listPositionOffsetA="
827                                 + -info.size);
828                     listPositionOffset = -info.size;
829                 }
830 
831                 // Setup adapter info
832                 info.start = data.start;
833                 info.end = data.end;
834                 info.cursor = cursor;
835                 info.dayAdapter.changeCursor(info);
836                 info.size = info.dayAdapter.getCount();
837 
838                 // Insert into adapterInfos
839                 if (mAdapterInfos.isEmpty()
840                         || data.end <= mAdapterInfos.getFirst().start) {
841                     mAdapterInfos.addFirst(info);
842                     listPositionOffset += info.size;
843                 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
844                     mAdapterInfos.addLast(info);
845                     for (DayAdapterInfo info2 : mAdapterInfos) {
846                         Log.e("========== BUG ==", info2.toString());
847                     }
848                 } else {
849                     mAdapterInfos.addLast(info);
850                 }
851 
852                 // Update offsets in adapterInfos
853                 mRowCount = 0;
854                 for (DayAdapterInfo info3 : mAdapterInfos) {
855                     info3.offset = mRowCount;
856                     mRowCount += info3.size;
857                 }
858                 mLastUsedInfo = null;
859 
860                 return listPositionOffset;
861             }
862         }
863     }
864 
getViewTitle(View x)865     static String getViewTitle(View x) {
866         String title = "";
867         if (x != null) {
868             Object yy = x.getTag();
869             if (yy instanceof AgendaAdapter.ViewHolder) {
870                 TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
871                 if (tv != null) {
872                     title = (String) tv.getText();
873                 }
874             } else if (yy != null) {
875                 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
876                 if (dateView != null) {
877                     title = (String) dateView.getText();
878                 }
879             }
880         }
881         return title;
882     }
883 
setHideDeclinedEvents(boolean hideDeclined)884     public void setHideDeclinedEvents(boolean hideDeclined) {
885         mHideDeclined = hideDeclined;
886     }
887 }
888