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