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