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