blob: 4dd2427580e8cc10437e85b3d907693286e535cf [file] [log] [blame]
/*
* Copyright (C) 2010 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.month;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.content.ContentUris;
import android.content.CursorLoader;
import android.content.Loader;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.StateListDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
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.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import com.android.calendar.CalendarController;
import com.android.calendar.CalendarController.EventInfo;
import com.android.calendar.CalendarController.EventType;
import com.android.calendar.CalendarController.ViewType;
import com.android.calendar.Event;
import com.android.calendar.R;
import com.android.calendar.Utils;
import com.android.calendar.event.CreateEventDialogFragment;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
public class MonthByWeekFragment extends SimpleDayPickerFragment implements
CalendarController.EventHandler, LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener,
OnTouchListener {
private static final String TAG = "MonthFragment";
private static final String TAG_EVENT_DIALOG = "event_dialog";
private CreateEventDialogFragment mEventDialog;
// Selection and selection args for adding event queries
private static final String WHERE_CALENDARS_VISIBLE = Calendars.VISIBLE + "=1";
private static final String INSTANCES_SORT_ORDER = Instances.START_DAY + ","
+ Instances.START_MINUTE + "," + Instances.TITLE;
protected static boolean mShowDetailsInMonth = false;
protected float mMinimumTwoMonthFlingVelocity;
protected boolean mIsMiniMonth;
protected boolean mHideDeclined;
protected int mFirstLoadedJulianDay;
protected int mLastLoadedJulianDay;
private static final int WEEKS_BUFFER = 1;
// How long to wait after scroll stops before starting the loader
// Using scroll duration because scroll state changes don't update
// correctly when a scroll is triggered programmatically.
private static final int LOADER_DELAY = 200;
// The minimum time between requeries of the data if the db is
// changing
private static final int LOADER_THROTTLE_DELAY = 500;
private CursorLoader mLoader;
private Uri mEventUri;
private final Time mDesiredDay = new Time();
private volatile boolean mShouldLoad = true;
private boolean mUserScrolled = false;
private int mEventsLoadingDelay;
private boolean mShowCalendarControls;
private boolean mIsDetached;
private Handler mEventDialogHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
final FragmentManager manager = getFragmentManager();
if (manager != null) {
Time day = (Time) msg.obj;
mEventDialog = new CreateEventDialogFragment(day);
mEventDialog.show(manager, TAG_EVENT_DIALOG);
}
}
};
private final Runnable mTZUpdater = new Runnable() {
@Override
public void run() {
String tz = Utils.getTimeZone(mContext, mTZUpdater);
mSelectedDay.timezone = tz;
mSelectedDay.normalize(true);
mTempTime.timezone = tz;
mFirstDayOfMonth.timezone = tz;
mFirstDayOfMonth.normalize(true);
mFirstVisibleDay.timezone = tz;
mFirstVisibleDay.normalize(true);
if (mAdapter != null) {
mAdapter.refresh();
}
}
};
private final Runnable mUpdateLoader = new Runnable() {
@Override
public void run() {
synchronized (this) {
if (!mShouldLoad || mLoader == null) {
return;
}
// Stop any previous loads while we update the uri
stopLoader();
// Start the loader again
mEventUri = updateUri();
mLoader.setUri(mEventUri);
mLoader.startLoading();
mLoader.onContentChanged();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Started loader with uri: " + mEventUri);
}
}
}
};
// Used to load the events when a delay is needed
Runnable mLoadingRunnable = new Runnable() {
@Override
public void run() {
if (!mIsDetached) {
mLoader = (CursorLoader) getLoaderManager().initLoader(0, null,
MonthByWeekFragment.this);
}
}
};
/**
* Updates the uri used by the loader according to the current position of
* the listview.
*
* @return The new Uri to use
*/
private Uri updateUri() {
SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0);
if (child != null) {
int julianDay = child.getFirstJulianDay();
mFirstLoadedJulianDay = julianDay;
}
// -1 to ensure we get all day events from any time zone
mTempTime.setJulianDay(mFirstLoadedJulianDay - 1);
long start = mTempTime.toMillis(true);
mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7;
// +1 to ensure we get all day events from any time zone
mTempTime.setJulianDay(mLastLoadedJulianDay + 1);
long end = mTempTime.toMillis(true);
// Create a new uri with the updated times
Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
ContentUris.appendId(builder, start);
ContentUris.appendId(builder, end);
return builder.build();
}
// Extract range of julian days from URI
private void updateLoadedDays() {
List<String> pathSegments = mEventUri.getPathSegments();
int size = pathSegments.size();
if (size <= 2) {
return;
}
long first = Long.parseLong(pathSegments.get(size - 2));
long last = Long.parseLong(pathSegments.get(size - 1));
mTempTime.set(first);
mFirstLoadedJulianDay = Time.getJulianDay(first, mTempTime.gmtoff);
mTempTime.set(last);
mLastLoadedJulianDay = Time.getJulianDay(last, mTempTime.gmtoff);
}
protected String updateWhere() {
// TODO fix selection/selection args after b/3206641 is fixed
String where = WHERE_CALENDARS_VISIBLE;
if (mHideDeclined || !mShowDetailsInMonth) {
where += " AND " + Instances.SELF_ATTENDEE_STATUS + "!="
+ Attendees.ATTENDEE_STATUS_DECLINED;
}
return where;
}
private void stopLoader() {
synchronized (mUpdateLoader) {
mHandler.removeCallbacks(mUpdateLoader);
if (mLoader != null) {
mLoader.stopLoading();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Stopped loader from loading");
}
}
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mTZUpdater.run();
if (mAdapter != null) {
mAdapter.setSelectedDay(mSelectedDay);
}
mIsDetached = false;
ViewConfiguration viewConfig = ViewConfiguration.get(activity);
mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2;
Resources res = activity.getResources();
mShowCalendarControls = Utils.getConfigBool(activity, R.bool.show_calendar_controls);
// Synchronized the loading time of the month's events with the animation of the
// calendar controls.
if (mShowCalendarControls) {
mEventsLoadingDelay = res.getInteger(R.integer.calendar_controls_animation_time);
}
mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month);
}
@Override
public void onDetach() {
mIsDetached = true;
super.onDetach();
if (mShowCalendarControls) {
if (mListView != null) {
mListView.removeCallbacks(mLoadingRunnable);
}
}
}
@Override
protected void setUpAdapter() {
mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
mShowWeekNumber = Utils.getShowWeekNumber(mContext);
HashMap<String, Integer> weekParams = new HashMap<String, Integer>();
weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks);
weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0);
weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek);
weekParams.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, mIsMiniMonth ? 1 : 0);
weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY,
Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff));
weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek);
if (mAdapter == null) {
mAdapter = new MonthByWeekAdapter(getActivity(), weekParams, mEventDialogHandler);
mAdapter.registerDataSetObserver(mObserver);
} else {
mAdapter.updateParams(weekParams);
}
mAdapter.notifyDataSetChanged();
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v;
if (mIsMiniMonth) {
v = inflater.inflate(R.layout.month_by_week, container, false);
} else {
v = inflater.inflate(R.layout.full_month_by_week, container, false);
}
mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names);
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mListView.setSelector(new StateListDrawable());
mListView.setOnTouchListener(this);
if (!mIsMiniMonth) {
mListView.setBackgroundColor(getResources().getColor(R.color.month_bgcolor));
}
// To get a smoother transition when showing this fragment, delay loading of events until
// the fragment is expended fully and the calendar controls are gone.
if (mShowCalendarControls) {
mListView.postDelayed(mLoadingRunnable, mEventsLoadingDelay);
} else {
mLoader = (CursorLoader) getLoaderManager().initLoader(0, null, this);
}
mAdapter.setListView(mListView);
}
public MonthByWeekFragment() {
this(System.currentTimeMillis(), true);
}
public MonthByWeekFragment(long initialTime, boolean isMiniMonth) {
super(initialTime);
mIsMiniMonth = isMiniMonth;
}
@Override
protected void setUpHeader() {
if (mIsMiniMonth) {
super.setUpHeader();
return;
}
mDayLabels = new String[7];
for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i,
DateUtils.LENGTH_MEDIUM).toUpperCase();
}
}
// TODO
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (mIsMiniMonth) {
return null;
}
CursorLoader loader;
synchronized (mUpdateLoader) {
mFirstLoadedJulianDay =
Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)
- (mNumWeeks * 7 / 2);
mEventUri = updateUri();
String where = updateWhere();
loader = new CursorLoader(
getActivity(), mEventUri, Event.EVENT_PROJECTION, where,
null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER);
loader.setUpdateThrottle(LOADER_THROTTLE_DELAY);
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Returning new loader with uri: " + mEventUri);
}
return loader;
}
@Override
public void doResumeUpdates() {
mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
mShowWeekNumber = Utils.getShowWeekNumber(mContext);
boolean prevHideDeclined = mHideDeclined;
mHideDeclined = Utils.getHideDeclinedEvents(mContext);
if (prevHideDeclined != mHideDeclined && mLoader != null) {
mLoader.setSelection(updateWhere());
}
mDaysPerWeek = Utils.getDaysPerWeek(mContext);
updateHeader();
mAdapter.setSelectedDay(mSelectedDay);
mTZUpdater.run();
mTodayUpdater.run();
goTo(mSelectedDay.toMillis(true), false, true, false);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
synchronized (mUpdateLoader) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri);
}
CursorLoader cLoader = (CursorLoader) loader;
if (mEventUri == null) {
mEventUri = cLoader.getUri();
updateLoadedDays();
}
if (cLoader.getUri().compareTo(mEventUri) != 0) {
// We've started a new query since this loader ran so ignore the
// result
return;
}
ArrayList<Event> events = new ArrayList<Event>();
Event.buildEventsFromCursor(
events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay);
((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay,
mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
@Override
public void eventsChanged() {
// TODO remove this after b/3387924 is resolved
if (mLoader != null) {
mLoader.forceLoad();
}
}
@Override
public long getSupportedEventTypes() {
return EventType.GO_TO | EventType.EVENTS_CHANGED;
}
@Override
public void handleEvent(EventInfo event) {
if (event.eventType == EventType.GO_TO) {
boolean animate = true;
if (mDaysPerWeek * mNumWeeks * 2 < Math.abs(
Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff)
- Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff)
- mDaysPerWeek * mNumWeeks / 2)) {
animate = false;
}
mDesiredDay.set(event.selectedTime);
mDesiredDay.normalize(true);
boolean animateToday = (event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0;
boolean delayAnimation = goTo(event.selectedTime.toMillis(true), animate, true, false);
if (animateToday) {
// If we need to flash today start the animation after any
// movement from listView has ended.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
((MonthByWeekAdapter) mAdapter).animateToday();
mAdapter.notifyDataSetChanged();
}
}, delayAnimation ? GOTO_SCROLL_DURATION : 0);
}
} else if (event.eventType == EventType.EVENTS_CHANGED) {
eventsChanged();
}
}
@Override
protected void setMonthDisplayed(Time time, boolean updateHighlight) {
super.setMonthDisplayed(time, updateHighlight);
if (!mIsMiniMonth) {
boolean useSelected = false;
if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) {
mSelectedDay.set(mDesiredDay);
mAdapter.setSelectedDay(mDesiredDay);
useSelected = true;
} else {
mSelectedDay.set(time);
mAdapter.setSelectedDay(time);
}
CalendarController controller = CalendarController.getInstance(mContext);
if (mSelectedDay.minute >= 30) {
mSelectedDay.minute = 30;
} else {
mSelectedDay.minute = 0;
}
long newTime = mSelectedDay.normalize(true);
if (newTime != controller.getTime() && mUserScrolled) {
long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3;
controller.setTime(newTime + offset);
}
controller.sendEvent(this, EventType.UPDATE_TITLE, time, time, time, -1,
ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
| DateUtils.FORMAT_SHOW_YEAR, null, null);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
synchronized (mUpdateLoader) {
if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
mShouldLoad = false;
stopLoader();
mDesiredDay.setToNow();
} else {
mHandler.removeCallbacks(mUpdateLoader);
mShouldLoad = true;
mHandler.postDelayed(mUpdateLoader, LOADER_DELAY);
}
}
if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
mUserScrolled = true;
}
mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
mDesiredDay.setToNow();
return false;
// TODO post a cleanup to push us back onto the grid if something went
// wrong in a scroll such as the user stopping the view but not
// scrolling
}
}