blob: 23d640fcfff72a540bb5cfd0406c7a52b3a57e23 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.calendar.agenda;
import com.android.calendar.CalendarController;
import com.android.calendar.R;
import com.android.calendar.Utils;
import com.android.calendar.CalendarController.EventType;
import com.android.calendar.CalendarController.ViewType;
import com.android.calendar.StickyHeaderListView;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Instances;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.Formatter;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.concurrent.ConcurrentLinkedQueue;
/*
Bugs Bugs Bugs:
- At rotation and launch time, the initial position is not set properly. This code is calling
listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
- Scroll using trackball isn't repositioning properly after a new adapter is added.
- Track ball clicks at the header/footer doesn't work.
- Potential ping pong effect if the prefetch window is big and data is limited
- Add index in calendar provider
ToDo ToDo ToDo:
Get design of header and footer from designer
Make scrolling smoother.
Test for correctness
Loading speed
Check for leaks and excessive allocations
*/
public class AgendaWindowAdapter extends BaseAdapter
implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener{
static final boolean BASICLOG = false;
static final boolean DEBUGLOG = false;
private static final String TAG = "AgendaWindowAdapter";
private static final String AGENDA_SORT_ORDER =
CalendarContract.Instances.START_DAY + " ASC, " +
CalendarContract.Instances.BEGIN + " ASC, " +
CalendarContract.Events.TITLE + " ASC";
public static final int INDEX_INSTANCE_ID = 0;
public static final int INDEX_TITLE = 1;
public static final int INDEX_EVENT_LOCATION = 2;
public static final int INDEX_ALL_DAY = 3;
public static final int INDEX_HAS_ALARM = 4;
public static final int INDEX_COLOR = 5;
public static final int INDEX_RRULE = 6;
public static final int INDEX_BEGIN = 7;
public static final int INDEX_END = 8;
public static final int INDEX_EVENT_ID = 9;
public static final int INDEX_START_DAY = 10;
public static final int INDEX_END_DAY = 11;
public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
public static final int INDEX_ORGANIZER = 13;
public static final int INDEX_OWNER_ACCOUNT = 14;
public static final int INDEX_CAN_ORGANIZER_RESPOND= 15;
public static final int INDEX_TIME_ZONE = 16;
private static final String[] PROJECTION = new String[] {
Instances._ID, // 0
Instances.TITLE, // 1
Instances.EVENT_LOCATION, // 2
Instances.ALL_DAY, // 3
Instances.HAS_ALARM, // 4
Instances.CALENDAR_COLOR, // 5
Instances.RRULE, // 6
Instances.BEGIN, // 7
Instances.END, // 8
Instances.EVENT_ID, // 9
Instances.START_DAY, // 10 Julian start day
Instances.END_DAY, // 11 Julian end day
Instances.SELF_ATTENDEE_STATUS, // 12
Instances.ORGANIZER, // 13
Instances.OWNER_ACCOUNT, // 14
Instances.CAN_ORGANIZER_RESPOND, // 15
Instances.EVENT_TIMEZONE, // 16
};
// Listview may have a bug where the index/position is not consistent when there's a header.
// position == positionInListView - OFF_BY_ONE_BUG
// TODO Need to look into this.
private static final int OFF_BY_ONE_BUG = 1;
private static final int MAX_NUM_OF_ADAPTERS = 5;
private static final int IDEAL_NUM_OF_EVENTS = 50;
private static final int MIN_QUERY_DURATION = 7; // days
private static final int MAX_QUERY_DURATION = 60; // days
private static final int PREFETCH_BOUNDARY = 1;
/** Times to auto-expand/retry query after getting no data */
private static final int RETRIES_ON_NO_DATA = 1;
private Context mContext;
private Resources mResources;
private QueryHandler mQueryHandler;
private AgendaListView mAgendaListView;
/** The sum of the rows in all the adapters */
private int mRowCount;
/** The number of times we have queried and gotten no results back */
private int mEmptyCursorCount;
/** Cached value of the last used adapter */
private DayAdapterInfo mLastUsedInfo;
private final LinkedList<DayAdapterInfo> mAdapterInfos =
new LinkedList<DayAdapterInfo>();
private final ConcurrentLinkedQueue<QuerySpec> mQueryQueue =
new ConcurrentLinkedQueue<QuerySpec>();
private TextView mHeaderView;
private TextView mFooterView;
private boolean mDoneSettingUpHeaderFooter = false;
private final boolean mIsTabletConfig;
boolean mCleanQueryInitiated = false;
private int mStickyHeaderSize = 44; // Initial size big enough for it to work
/**
* When the user scrolled to the top, a query will be made for older events
* and this will be incremented. Don't make more requests if
* mOlderRequests > mOlderRequestsProcessed.
*/
private int mOlderRequests;
/** Number of "older" query that has been processed. */
private int mOlderRequestsProcessed;
/**
* When the user scrolled to the bottom, a query will be made for newer
* events and this will be incremented. Don't make more requests if
* mNewerRequests > mNewerRequestsProcessed.
*/
private int mNewerRequests;
/** Number of "newer" query that has been processed. */
private int mNewerRequestsProcessed;
// Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
private Formatter mFormatter;
private StringBuilder mStringBuilder;
private String mTimeZone;
// defines if to pop-up the current event when the agenda is first shown
private boolean mShowEventOnStart;
private Runnable mTZUpdater = new Runnable() {
@Override
public void run() {
mTimeZone = Utils.getTimeZone(mContext, this);
notifyDataSetChanged();
}
};
private boolean mShuttingDown;
private boolean mHideDeclined;
// Used to stop a fling motion if the ListView is set to a specific position
int mListViewScrollState = OnScrollListener.SCROLL_STATE_IDLE;
/** The current search query, or null if none */
private String mSearchQuery;
private long mSelectedInstanceId = -1;
private final int mSelectedItemBackgroundColor;
private final int mSelectedItemTextColor;
// Types of Query
private static final int QUERY_TYPE_OLDER = 0; // Query for older events
private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
private static class QuerySpec {
long queryStartMillis;
Time goToTime;
int start;
int end;
String searchQuery;
int queryType;
long id;
public QuerySpec(int queryType) {
this.queryType = queryType;
id = -1;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + end;
result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
result = prime * result + queryType;
result = prime * result + start;
if (searchQuery != null) {
result = prime * result + searchQuery.hashCode();
}
if (goToTime != null) {
long goToTimeMillis = goToTime.toMillis(false);
result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
}
result = prime * result + (int)id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
QuerySpec other = (QuerySpec) obj;
if (end != other.end || queryStartMillis != other.queryStartMillis
|| queryType != other.queryType || start != other.start
|| Utils.equals(searchQuery, other.searchQuery) || id != other.id) {
return false;
}
if (goToTime != null) {
if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
return false;
}
} else {
if (other.goToTime != null) {
return false;
}
}
return true;
}
}
static class EventInfo {
long begin;
long end;
long id;
int startDay;
boolean allDay;
}
static class DayAdapterInfo {
Cursor cursor;
AgendaByDayAdapter dayAdapter;
int start; // start day of the cursor's coverage
int end; // end day of the cursor's coverage
int offset; // offset in position in the list view
int size; // dayAdapter.getCount()
public DayAdapterInfo(Context context) {
dayAdapter = new AgendaByDayAdapter(context);
}
@Override
public String toString() {
// Static class, so the time in this toString will not reflect the
// home tz settings. This should only affect debugging.
Time time = new Time();
StringBuilder sb = new StringBuilder();
time.setJulianDay(start);
time.normalize(false);
sb.append("Start:").append(time.toString());
time.setJulianDay(end);
time.normalize(false);
sb.append(" End:").append(time.toString());
sb.append(" Offset:").append(offset);
sb.append(" Size:").append(size);
return sb.toString();
}
}
public AgendaWindowAdapter(Context context,
AgendaListView agendaListView, boolean showEventOnStart) {
mContext = context;
mResources = context.getResources();
mSelectedItemBackgroundColor = mResources
.getColor(R.color.agenda_selected_background_color);
mSelectedItemTextColor = mResources.getColor(R.color.agenda_selected_text_color);
mIsTabletConfig = Utils.getConfigBool(mContext, R.bool.tablet_config);
mTimeZone = Utils.getTimeZone(context, mTZUpdater);
mAgendaListView = agendaListView;
mQueryHandler = new QueryHandler(context.getContentResolver());
mStringBuilder = new StringBuilder(50);
mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
mShowEventOnStart = showEventOnStart;
// Implies there is no sticky header
if (!mShowEventOnStart) {
mStickyHeaderSize = 0;
}
mSearchQuery = null;
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
mHeaderView.setText(R.string.loading);
mAgendaListView.addHeaderView(mHeaderView);
}
// Method in Adapter
@Override
public int getViewTypeCount() {
return AgendaByDayAdapter.TYPE_LAST;
}
// Method in BaseAdapter
@Override
public boolean areAllItemsEnabled() {
return false;
}
// Method in Adapter
@Override
public int getItemViewType(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.dayAdapter.getItemViewType(position - info.offset);
} else {
return -1;
}
}
// Method in BaseAdapter
@Override
public boolean isEnabled(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.dayAdapter.isEnabled(position - info.offset);
} else {
return false;
}
}
// Abstract Method in BaseAdapter
public int getCount() {
return mRowCount;
}
// Abstract Method in BaseAdapter
public Object getItem(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.dayAdapter.getItem(position - info.offset);
} else {
return null;
}
}
// Method in BaseAdapter
@Override
public boolean hasStableIds() {
return true;
}
// Abstract Method in BaseAdapter
@Override
public long getItemId(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
int curPos = info.dayAdapter.getCursorPosition(position - info.offset);
if (curPos == Integer.MIN_VALUE) {
return -1;
}
// Regular event
if (curPos >= 0) {
info.cursor.moveToPosition(curPos);
return info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID) << 20 +
info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
}
// Day Header
return info.dayAdapter.findJulianDayFromPosition(position);
} else {
return -1;
}
}
// Abstract Method in BaseAdapter
public View getView(int position, View convertView, ViewGroup parent) {
if (position >= (mRowCount - PREFETCH_BOUNDARY)
&& mNewerRequests <= mNewerRequestsProcessed) {
if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
mNewerRequests++;
queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
}
if (position < PREFETCH_BOUNDARY
&& mOlderRequests <= mOlderRequestsProcessed) {
if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
mOlderRequests++;
queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
}
final View v;
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
int offset = position - info.offset;
v = info.dayAdapter.getView(offset, convertView,
parent);
// Turn on the past/present separator if the view is a day header
// and it is the first day with events after yesterday.
if (info.dayAdapter.isDayHeaderView(offset)) {
View simpleDivider = v.findViewById(R.id.top_divider_simple);
View pastPresentDivider = v.findViewById(R.id.top_divider_past_present);
if (info.dayAdapter.isFirstDayAfterYesterday(offset)) {
if (simpleDivider != null && pastPresentDivider != null) {
simpleDivider.setVisibility(View.GONE);
pastPresentDivider.setVisibility(View.VISIBLE);
}
} else if (simpleDivider != null && pastPresentDivider != null) {
simpleDivider.setVisibility(View.VISIBLE);
pastPresentDivider.setVisibility(View.GONE);
}
}
} else {
// TODO
Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
TextView tv = new TextView(mContext);
tv.setText("Bug! " + position);
v = tv;
}
// If this is not a tablet config don't do selection highlighting
if (!mIsTabletConfig) {
return v;
}
// Show selected marker if this is item is selected
boolean selected = false;
Object yy = v.getTag();
if (yy instanceof AgendaAdapter.ViewHolder) {
AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) yy;
selected = mSelectedInstanceId == vh.instanceId;
vh.selectedMarker.setVisibility((selected && mShowEventOnStart) ?
View.VISIBLE : View.GONE);
if (selected && mShowEventOnStart) {
mSelectedVH = vh;
v.setBackgroundColor(mSelectedItemBackgroundColor);
vh.title.setTextColor(mSelectedItemTextColor);
vh.when.setTextColor(mSelectedItemTextColor);
vh.where.setTextColor(mSelectedItemTextColor);
}
}
if (DEBUGLOG) {
Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
}
return v;
}
private AgendaAdapter.ViewHolder mSelectedVH = null;
private int findEventPositionNearestTime(Time time, long id) {
if (DEBUGLOG) Log.e(TAG, "findEventPositionNearestTime " + time + " id " + id);
DayAdapterInfo info = getAdapterInfoByTime(time);
if (info != null) {
return info.offset + info.dayAdapter.findEventPositionNearestTime(time, id);
} else {
return -1;
}
}
protected DayAdapterInfo getAdapterInfoByPosition(int position) {
synchronized (mAdapterInfos) {
if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
&& position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
return mLastUsedInfo;
}
for (DayAdapterInfo info : mAdapterInfos) {
if (info.offset <= position
&& position < (info.offset + info.size)) {
mLastUsedInfo = info;
return info;
}
}
}
return null;
}
private DayAdapterInfo getAdapterInfoByTime(Time time) {
if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
Time tmpTime = new Time(time);
long timeInMillis = tmpTime.normalize(true);
int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
synchronized (mAdapterInfos) {
for (DayAdapterInfo info : mAdapterInfos) {
if (info.start <= day && day <= info.end) {
return info;
}
}
}
return null;
}
public EventInfo getEventByPosition(final int positionInListView) {
return getEventByPosition(positionInListView, true);
}
/**
* Return the event info for a given position in the adapter
* @param positionInListView
* @param returnEventStartDay If true, return actual event startday. Otherwise
* return agenda date-header date as the startDay.
* The two will differ for multi-day events after the first day.
* @return
*/
public EventInfo getEventByPosition(final int positionInListView,
boolean returnEventStartDay) {
if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + positionInListView);
if (positionInListView < 0) {
return null;
}
final int positionInAdapter = positionInListView - OFF_BY_ONE_BUG;
DayAdapterInfo info = getAdapterInfoByPosition(positionInAdapter);
if (info == null) {
return null;
}
int cursorPosition = info.dayAdapter.getCursorPosition(positionInAdapter - info.offset);
if (cursorPosition == Integer.MIN_VALUE) {
return null;
}
boolean isDayHeader = false;
if (cursorPosition < 0) {
cursorPosition = -cursorPosition;
isDayHeader = true;
}
if (cursorPosition < info.cursor.getCount()) {
EventInfo ei = buildEventInfoFromCursor(info.cursor, cursorPosition, isDayHeader);
if (!returnEventStartDay && !isDayHeader) {
ei.startDay = info.dayAdapter.findJulianDayFromPosition(positionInAdapter -
info.offset);
}
return ei;
}
return null;
}
private EventInfo buildEventInfoFromCursor(final Cursor cursor, int cursorPosition,
boolean isDayHeader) {
if (cursorPosition == -1) {
cursor.moveToFirst();
} else {
cursor.moveToPosition(cursorPosition);
}
EventInfo event = new EventInfo();
event.begin = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END);
event.startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
event.allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
if (event.allDay) { // UTC
Time time = new Time(mTimeZone);
time.setJulianDay(Time.getJulianDay(event.begin, 0));
event.begin = time.toMillis(false /* use isDst */);
} else if (isDayHeader) { // Trim to midnight.
Time time = new Time(mTimeZone);
time.set(event.begin);
time.hour = 0;
time.minute = 0;
time.second = 0;
event.begin = time.toMillis(false /* use isDst */);
}
if (!isDayHeader) {
if (event.allDay) {
Time time = new Time(mTimeZone);
time.setJulianDay(Time.getJulianDay(event.end, 0));
event.end = time.toMillis(false /* use isDst */);
} else {
event.end = cursor.getLong(AgendaWindowAdapter.INDEX_END);
}
event.id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
}
return event;
}
public void refresh(Time goToTime, long id, String searchQuery, boolean forced,
boolean refreshEventInfo) {
if (searchQuery != null) {
mSearchQuery = searchQuery;
}
if (DEBUGLOG) {
Log.e(TAG, this + ": refresh " + goToTime.toString() + " id " + id
+ ((searchQuery != null) ? searchQuery : "")
+ (forced ? " forced" : " not forced")
+ (refreshEventInfo ? " refresh event info" : ""));
}
int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
if (!forced && isInRange(startDay, startDay)) {
// No need to re-query
if (!mAgendaListView.isEventVisible(goToTime, id)) {
int gotoPosition = findEventPositionNearestTime(goToTime, id);
if (gotoPosition > 0) {
mAgendaListView.setSelectionFromTop(gotoPosition +
OFF_BY_ONE_BUG, mStickyHeaderSize);
if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) {
mAgendaListView.smoothScrollBy(0, 0);
}
if (refreshEventInfo) {
long newInstanceId = findInstanceIdFromPosition(gotoPosition);
if (newInstanceId != getSelectedInstanceId()) {
setSelectedInstanceId(newInstanceId);
new Handler().post(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
Cursor tempCursor = getCursorByPosition(gotoPosition);
if (tempCursor != null) {
int tempCursorPosition = getCursorPositionByPosition(gotoPosition);
EventInfo event =
buildEventInfoFromCursor(tempCursor, tempCursorPosition,
false);
CalendarController.getInstance(mContext).sendEventRelatedEvent(
this, EventType.VIEW_EVENT, event.id, event.begin,
event.end, 0, 0, -1);
}
}
}
}
Time actualTime = new Time(mTimeZone);
if (goToTime != null) {
actualTime.set(goToTime);
} else {
actualTime.set(mAgendaListView.getFirstVisibleTime());
}
CalendarController.getInstance(mContext).sendEvent(this, EventType.UPDATE_TITLE,
actualTime, actualTime, -1, ViewType.CURRENT);
}
return;
}
// If AllInOneActivity is sending a second GOTO event(in OnResume), ignore it.
if (!mCleanQueryInitiated || searchQuery != null) {
// Query for a total of MIN_QUERY_DURATION days
int endDay = startDay + MIN_QUERY_DURATION;
mSelectedInstanceId = -1;
queueQuery(startDay, endDay, goToTime, searchQuery, QUERY_TYPE_CLEAN, id);
mCleanQueryInitiated = true;
// Pre-fetch more data to overcome a race condition in AgendaListView.shiftSelection
// Queuing more data with the goToTime set to the selected time skips the call to
// shiftSelection on refresh.
mOlderRequests++;
queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_OLDER, id);
mNewerRequests++;
queueQuery(0, 0, goToTime, searchQuery, QUERY_TYPE_NEWER, id);
}
}
public void close() {
mShuttingDown = true;
pruneAdapterInfo(QUERY_TYPE_CLEAN);
if (mQueryHandler != null) {
mQueryHandler.cancelOperation(0);
}
}
private DayAdapterInfo pruneAdapterInfo(int queryType) {
synchronized (mAdapterInfos) {
DayAdapterInfo recycleMe = null;
if (!mAdapterInfos.isEmpty()) {
if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
if (queryType == QUERY_TYPE_NEWER) {
recycleMe = mAdapterInfos.removeFirst();
} else if (queryType == QUERY_TYPE_OLDER) {
recycleMe = mAdapterInfos.removeLast();
// Keep the size only if the oldest items are removed.
recycleMe.size = 0;
}
if (recycleMe != null) {
if (recycleMe.cursor != null) {
recycleMe.cursor.close();
}
return recycleMe;
}
}
if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
mRowCount = 0;
int deletedRows = 0;
DayAdapterInfo info;
do {
info = mAdapterInfos.poll();
if (info != null) {
// TODO the following causes ANR's. Do this in a thread.
info.cursor.close();
deletedRows += info.size;
recycleMe = info;
}
} while (info != null);
if (recycleMe != null) {
recycleMe.cursor = null;
recycleMe.size = deletedRows;
}
}
}
return recycleMe;
}
}
private String buildQuerySelection() {
// Respect the preference to show/hide declined events
if (mHideDeclined) {
return Calendars.VISIBLE + "=1 AND "
+ Instances.SELF_ATTENDEE_STATUS + "!="
+ Attendees.ATTENDEE_STATUS_DECLINED;
} else {
return Calendars.VISIBLE + "=1";
}
}
private Uri buildQueryUri(int start, int end, String searchQuery) {
Uri rootUri = searchQuery == null ?
Instances.CONTENT_BY_DAY_URI :
Instances.CONTENT_SEARCH_BY_DAY_URI;
Uri.Builder builder = rootUri.buildUpon();
ContentUris.appendId(builder, start);
ContentUris.appendId(builder, end);
if (searchQuery != null) {
builder.appendPath(searchQuery);
}
return builder.build();
}
private boolean isInRange(int start, int end) {
synchronized (mAdapterInfos) {
if (mAdapterInfos.isEmpty()) {
return false;
}
return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
}
}
private int calculateQueryDuration(int start, int end) {
int queryDuration = MAX_QUERY_DURATION;
if (mRowCount != 0) {
queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
}
if (queryDuration > MAX_QUERY_DURATION) {
queryDuration = MAX_QUERY_DURATION;
} else if (queryDuration < MIN_QUERY_DURATION) {
queryDuration = MIN_QUERY_DURATION;
}
return queryDuration;
}
private boolean queueQuery(int start, int end, Time goToTime,
String searchQuery, int queryType, long id) {
QuerySpec queryData = new QuerySpec(queryType);
queryData.goToTime = goToTime;
queryData.start = start;
queryData.end = end;
queryData.searchQuery = searchQuery;
queryData.id = id;
return queueQuery(queryData);
}
private boolean queueQuery(QuerySpec queryData) {
queryData.searchQuery = mSearchQuery;
Boolean queuedQuery;
synchronized (mQueryQueue) {
queuedQuery = false;
Boolean doQueryNow = mQueryQueue.isEmpty();
mQueryQueue.add(queryData);
queuedQuery = true;
if (doQueryNow) {
doQuery(queryData);
}
}
return queuedQuery;
}
private void doQuery(QuerySpec queryData) {
if (!mAdapterInfos.isEmpty()) {
int start = mAdapterInfos.getFirst().start;
int end = mAdapterInfos.getLast().end;
int queryDuration = calculateQueryDuration(start, end);
switch(queryData.queryType) {
case QUERY_TYPE_OLDER:
queryData.end = start - 1;
queryData.start = queryData.end - queryDuration;
break;
case QUERY_TYPE_NEWER:
queryData.start = end + 1;
queryData.end = queryData.start + queryDuration;
break;
}
// By "compacting" cursors, this fixes the disco/ping-pong problem
// b/5311977
if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) {
if (DEBUGLOG) {
Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount
+ " totalStart:" + start
+ " totalEnd:" + end
+ " query.start:" + queryData.start
+ " query.end:" + queryData.end);
}
queryData.queryType = QUERY_TYPE_CLEAN;
if (queryData.start > start) {
queryData.start = start;
}
if (queryData.end < end) {
queryData.end = end;
}
}
}
if (BASICLOG) {
Time time = new Time(mTimeZone);
time.setJulianDay(queryData.start);
Time time2 = new Time(mTimeZone);
time2.setJulianDay(queryData.end);
Log.v(TAG, "startQuery: " + time.toString() + " to "
+ time2.toString() + " then go to " + queryData.goToTime);
}
mQueryHandler.cancelOperation(0);
if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
Uri queryUri = buildQueryUri(
queryData.start, queryData.end, queryData.searchQuery);
mQueryHandler.startQuery(0, queryData, queryUri,
PROJECTION, buildQuerySelection(), null,
AGENDA_SORT_ORDER);
}
private String formatDateString(int julianDay) {
Time time = new Time(mTimeZone);
time.setJulianDay(julianDay);
long millis = time.toMillis(false);
mStringBuilder.setLength(0);
return DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_ABBREV_MONTH, mTimeZone).toString();
}
private void updateHeaderFooter(final int start, final int end) {
mHeaderView.setText(mContext.getString(R.string.show_older_events,
formatDateString(start)));
mFooterView.setText(mContext.getString(R.string.show_newer_events,
formatDateString(end)));
}
private class QueryHandler extends AsyncQueryHandler {
public QueryHandler(ContentResolver cr) {
super(cr);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
QuerySpec data = (QuerySpec)cookie;
if (BASICLOG) {
long queryEndMillis = System.nanoTime();
Log.e(TAG, "Query time(ms): "
+ (queryEndMillis - data.queryStartMillis) / 1000000
+ " Count: " + cursor.getCount());
}
if (data.queryType == QUERY_TYPE_CLEAN) {
mCleanQueryInitiated = false;
}
if (mShuttingDown) {
cursor.close();
return;
}
// Notify Listview of changes and update position
int cursorSize = cursor.getCount();
if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
final int listPositionOffset = processNewCursor(data, cursor);
int newPosition = -1;
if (data.goToTime == null) { // Typical Scrolling type query
notifyDataSetChanged();
if (listPositionOffset != 0) {
mAgendaListView.shiftSelection(listPositionOffset);
}
} else { // refresh() called. Go to the designated position
final Time goToTime = data.goToTime;
notifyDataSetChanged();
newPosition = findEventPositionNearestTime(goToTime, data.id);
if (newPosition >= 0) {
if (mListViewScrollState == OnScrollListener.SCROLL_STATE_FLING) {
mAgendaListView.smoothScrollBy(0, 0);
}
mAgendaListView.setSelectionFromTop(newPosition + OFF_BY_ONE_BUG,
mStickyHeaderSize);
Time actualTime = new Time(mTimeZone);
actualTime.set(goToTime);
CalendarController.getInstance(mContext).sendEvent(this,
EventType.UPDATE_TITLE, actualTime, actualTime, -1,
ViewType.CURRENT);
}
if (DEBUGLOG) {
Log.e(TAG, "Setting listview to " +
"findEventPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
}
}
// Make sure we change the selected instance Id only on a clean query and we
// do not have one set already
if (mSelectedInstanceId == -1 && newPosition != -1 &&
data.queryType == QUERY_TYPE_CLEAN) {
if (data.id != -1 || data.goToTime != null) {
mSelectedInstanceId = findInstanceIdFromPosition(newPosition);
}
}
// size == 1 means a fresh query. Possibly after the data changed.
// Let's check whether mSelectedInstanceId is still valid.
if (mAdapterInfos.size() == 1 && mSelectedInstanceId != -1) {
boolean found = false;
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
if (mSelectedInstanceId == cursor
.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID)) {
found = true;
break;
}
};
if (!found) {
mSelectedInstanceId = -1;
}
}
// Show the requested event
if (mShowEventOnStart && data.queryType == QUERY_TYPE_CLEAN) {
Cursor tempCursor = null;
int tempCursorPosition = -1;
// If no valid event is selected , just pick the first one
if (mSelectedInstanceId == -1) {
if (cursor.moveToFirst()) {
mSelectedInstanceId = cursor
.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
// Set up a dummy view holder so we have the right all day
// info when the view is created.
// TODO determine the full set of what might be useful to
// know about the selected view and fill it in.
mSelectedVH = new AgendaAdapter.ViewHolder();
mSelectedVH.allDay =
cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
tempCursor = cursor;
}
} else if (newPosition != -1) {
tempCursor = getCursorByPosition(newPosition);
tempCursorPosition = getCursorPositionByPosition(newPosition);
}
if (tempCursor != null) {
EventInfo event = buildEventInfoFromCursor(tempCursor, tempCursorPosition,
false);
CalendarController.getInstance(mContext).sendEventRelatedEvent(this,
EventType.VIEW_EVENT, event.id, event.begin, event.end, 0, 0, -1);
}
}
} else {
cursor.close();
}
// Update header and footer
if (!mDoneSettingUpHeaderFooter) {
OnClickListener headerFooterOnClickListener = new OnClickListener() {
public void onClick(View v) {
if (v == mHeaderView) {
queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
} else {
queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
}
}};
mHeaderView.setOnClickListener(headerFooterOnClickListener);
mFooterView.setOnClickListener(headerFooterOnClickListener);
mAgendaListView.addFooterView(mFooterView);
mDoneSettingUpHeaderFooter = true;
}
synchronized (mQueryQueue) {
int totalAgendaRangeStart = -1;
int totalAgendaRangeEnd = -1;
if (cursorSize != 0) {
// Remove the query that just completed
QuerySpec x = mQueryQueue.poll();
if (BASICLOG && !x.equals(data)) {
Log.e(TAG, "onQueryComplete - cookie != head of queue");
}
mEmptyCursorCount = 0;
if (data.queryType == QUERY_TYPE_NEWER) {
mNewerRequestsProcessed++;
} else if (data.queryType == QUERY_TYPE_OLDER) {
mOlderRequestsProcessed++;
}
totalAgendaRangeStart = mAdapterInfos.getFirst().start;
totalAgendaRangeEnd = mAdapterInfos.getLast().end;
} else { // CursorSize == 0
QuerySpec querySpec = mQueryQueue.peek();
// Update Adapter Info with new start and end date range
if (!mAdapterInfos.isEmpty()) {
DayAdapterInfo first = mAdapterInfos.getFirst();
DayAdapterInfo last = mAdapterInfos.getLast();
if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
first.start = querySpec.start;
}
if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
last.end = querySpec.end;
}
totalAgendaRangeStart = first.start;
totalAgendaRangeEnd = last.end;
} else {
totalAgendaRangeStart = querySpec.start;
totalAgendaRangeEnd = querySpec.end;
}
// Update query specification with expanded search range
// and maybe rerun query
switch (querySpec.queryType) {
case QUERY_TYPE_OLDER:
totalAgendaRangeStart = querySpec.start;
querySpec.start -= MAX_QUERY_DURATION;
break;
case QUERY_TYPE_NEWER:
totalAgendaRangeEnd = querySpec.end;
querySpec.end += MAX_QUERY_DURATION;
break;
case QUERY_TYPE_CLEAN:
totalAgendaRangeStart = querySpec.start;
totalAgendaRangeEnd = querySpec.end;
querySpec.start -= MAX_QUERY_DURATION / 2;
querySpec.end += MAX_QUERY_DURATION / 2;
break;
}
if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
// Nothing in the cursor again. Dropping query
mQueryQueue.poll();
}
}
updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
// Go over the events and mark the first day after yesterday
// that has events in it
synchronized (mAdapterInfos) {
DayAdapterInfo info = mAdapterInfos.getFirst();
if (info != null) {
Time time = new Time(mTimeZone);
long now = System.currentTimeMillis();
time.set(now);
int JulianToday = Time.getJulianDay(now, time.gmtoff);
Iterator<DayAdapterInfo> iter = mAdapterInfos.iterator();
boolean foundDay = false;
while (iter.hasNext() && !foundDay) {
info = iter.next();
for (int i = 0; i < info.size; i++) {
if (info.dayAdapter.findJulianDayFromPosition(i) >= JulianToday) {
info.dayAdapter.setAsFirstDayAfterYesterday(i);
foundDay = true;
break;
}
}
}
}
}
// Fire off the next query if any
Iterator<QuerySpec> it = mQueryQueue.iterator();
while (it.hasNext()) {
QuerySpec queryData = it.next();
if (!isInRange(queryData.start, queryData.end)) {
// Query accepted
if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
doQuery(queryData);
break;
} else {
// Query rejected
it.remove();
if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
}
}
}
if (BASICLOG) {
for (DayAdapterInfo info3 : mAdapterInfos) {
Log.e(TAG, "> " + info3.toString());
}
}
}
/*
* Update the adapter info array with a the new cursor. Close out old
* cursors as needed.
*
* @return number of rows removed from the beginning
*/
private int processNewCursor(QuerySpec data, Cursor cursor) {
synchronized (mAdapterInfos) {
// Remove adapter info's from adapterInfos as needed
DayAdapterInfo info = pruneAdapterInfo(data.queryType);
int listPositionOffset = 0;
if (info == null) {
info = new DayAdapterInfo(mContext);
} else {
if (DEBUGLOG)
Log.e(TAG, "processNewCursor listPositionOffsetA="
+ -info.size);
listPositionOffset = -info.size;
}
// Setup adapter info
info.start = data.start;
info.end = data.end;
info.cursor = cursor;
info.dayAdapter.changeCursor(info);
info.size = info.dayAdapter.getCount();
// Insert into adapterInfos
if (mAdapterInfos.isEmpty()
|| data.end <= mAdapterInfos.getFirst().start) {
mAdapterInfos.addFirst(info);
listPositionOffset += info.size;
} else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
mAdapterInfos.addLast(info);
for (DayAdapterInfo info2 : mAdapterInfos) {
Log.e("========== BUG ==", info2.toString());
}
} else {
mAdapterInfos.addLast(info);
}
// Update offsets in adapterInfos
mRowCount = 0;
for (DayAdapterInfo info3 : mAdapterInfos) {
info3.offset = mRowCount;
mRowCount += info3.size;
}
mLastUsedInfo = null;
return listPositionOffset;
}
}
}
static String getViewTitle(View x) {
String title = "";
if (x != null) {
Object yy = x.getTag();
if (yy instanceof AgendaAdapter.ViewHolder) {
TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
if (tv != null) {
title = (String) tv.getText();
}
} else if (yy != null) {
TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
if (dateView != null) {
title = (String) dateView.getText();
}
}
}
return title;
}
public void onResume() {
mTZUpdater.run();
}
public void setHideDeclinedEvents(boolean hideDeclined) {
mHideDeclined = hideDeclined;
}
public void setSelectedView(View v) {
if (v != null) {
Object vh = v.getTag();
if (vh instanceof AgendaAdapter.ViewHolder) {
mSelectedVH = (AgendaAdapter.ViewHolder) vh;
mSelectedInstanceId = mSelectedVH.instanceId;
}
}
}
public AgendaAdapter.ViewHolder getSelectedViewHolder() {
return mSelectedVH;
}
public long getSelectedInstanceId() {
return mSelectedInstanceId;
}
public void setSelectedInstanceId(long selectedInstanceId) {
mSelectedInstanceId = selectedInstanceId;
mSelectedVH = null;
}
private long findInstanceIdFromPosition(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.dayAdapter.getInstanceId(position - info.offset);
}
return -1;
}
private Cursor getCursorByPosition(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.cursor;
}
return null;
}
private int getCursorPositionByPosition(int position) {
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
return info.dayAdapter.getCursorPosition(position - info.offset);
}
return -1;
}
// Implementation of HeaderIndexer interface for StickyHeeaderListView
// Returns the location of the day header of a specific event specified in the position
// in the adapter
@Override
public int getHeaderPositionFromItemPosition(int position) {
// For phone configuration, return -1 so there will be no sticky header
if (!mIsTabletConfig) {
return -1;
}
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
int pos = info.dayAdapter.getHeaderPosition(position - info.offset);
return (pos != -1)?(pos + info.offset):-1;
}
return -1;
}
// Returns the number of events for a specific day header
@Override
public int getHeaderItemsNumber(int headerPosition) {
if (headerPosition < 0 || !mIsTabletConfig) {
return -1;
}
DayAdapterInfo info = getAdapterInfoByPosition(headerPosition);
if (info != null) {
return info.dayAdapter.getHeaderItemsCount(headerPosition - info.offset);
}
return -1;
}
@Override
public void OnHeaderHeightChanged(int height) {
mStickyHeaderSize = height;
}
public int getStickyHeaderHeight() {
return mStickyHeaderSize;
}
public void setScrollState(int state) {
mListViewScrollState = state;
}
}