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