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