blob: a06774b8d229060db288b47267ca801d74a13f21 [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.event;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.ProgressDialog;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.provider.Settings;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.MultiAutoCompleteTextView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ResourceCursorAdapter;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.android.calendar.CalendarEventModel;
import com.android.calendar.CalendarEventModel.Attendee;
import com.android.calendar.CalendarEventModel.ReminderEntry;
import com.android.calendar.EmailAddressAdapter;
import com.android.calendar.EventInfoFragment;
import com.android.calendar.EventRecurrenceFormatter;
import com.android.calendar.GeneralPreferences;
import com.android.calendar.R;
import com.android.calendar.RecipientAdapter;
import com.android.calendar.Utils;
import com.android.calendar.event.EditEventHelper.EditDoneRunnable;
import com.android.calendar.recurrencepicker.RecurrencePickerDialog;
import com.android.calendarcommon2.EventRecurrence;
import com.android.common.Rfc822InputFilter;
import com.android.common.Rfc822Validator;
import com.android.datetimepicker.date.DatePickerDialog;
import com.android.datetimepicker.date.DatePickerDialog.OnDateSetListener;
import com.android.datetimepicker.time.RadialPickerLayout;
import com.android.datetimepicker.time.TimePickerDialog;
import com.android.datetimepicker.time.TimePickerDialog.OnTimeSetListener;
import com.android.ex.chips.AccountSpecifier;
import com.android.ex.chips.BaseRecipientAdapter;
import com.android.ex.chips.ChipsUtil;
import com.android.ex.chips.RecipientEditTextView;
import com.android.timezonepicker.TimeZoneInfo;
import com.android.timezonepicker.TimeZonePickerDialog;
import com.android.timezonepicker.TimeZonePickerUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener,
DialogInterface.OnClickListener, OnItemSelectedListener,
RecurrencePickerDialog.OnRecurrenceSetListener,
TimeZonePickerDialog.OnTimeZoneSetListener {
private static final String TAG = "EditEvent";
private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com";
private static final String PERIOD_SPACE = ". ";
private static final String FRAG_TAG_DATE_PICKER = "datePickerDialogFragment";
private static final String FRAG_TAG_TIME_PICKER = "timePickerDialogFragment";
private static final String FRAG_TAG_TIME_ZONE_PICKER = "timeZonePickerDialogFragment";
private static final String FRAG_TAG_RECUR_PICKER = "recurrencePickerDialogFragment";
ArrayList<View> mEditOnlyList = new ArrayList<View>();
ArrayList<View> mEditViewList = new ArrayList<View>();
ArrayList<View> mViewOnlyList = new ArrayList<View>();
TextView mLoadingMessage;
ScrollView mScrollView;
Button mStartDateButton;
Button mEndDateButton;
Button mStartTimeButton;
Button mEndTimeButton;
Button mTimezoneButton;
View mColorPickerNewEvent;
View mColorPickerExistingEvent;
OnClickListener mChangeColorOnClickListener;
View mTimezoneRow;
TextView mStartTimeHome;
TextView mStartDateHome;
TextView mEndTimeHome;
TextView mEndDateHome;
CheckBox mAllDayCheckBox;
Spinner mCalendarsSpinner;
Button mRruleButton;
Spinner mAvailabilitySpinner;
Spinner mAccessLevelSpinner;
RadioGroup mResponseRadioGroup;
TextView mTitleTextView;
AutoCompleteTextView mLocationTextView;
EventLocationAdapter mLocationAdapter;
TextView mDescriptionTextView;
TextView mWhenView;
TextView mTimezoneTextView;
TextView mTimezoneLabel;
LinearLayout mRemindersContainer;
MultiAutoCompleteTextView mAttendeesList;
View mCalendarSelectorGroup;
View mCalendarSelectorWrapper;
View mCalendarStaticGroup;
View mLocationGroup;
View mDescriptionGroup;
View mRemindersGroup;
View mResponseGroup;
View mOrganizerGroup;
View mAttendeesGroup;
View mStartHomeGroup;
View mEndHomeGroup;
private int[] mOriginalPadding = new int[4];
public boolean mIsMultipane;
private ProgressDialog mLoadingCalendarsDialog;
private AlertDialog mNoCalendarsDialog;
private DialogFragment mTimezoneDialog;
private Activity mActivity;
private EditDoneRunnable mDone;
private View mView;
private CalendarEventModel mModel;
private Cursor mCalendarsCursor;
private AccountSpecifier mAddressAdapter;
private Rfc822Validator mEmailValidator;
public boolean mTimeSelectedWasStartTime;
public boolean mDateSelectedWasStartDate;
private TimePickerDialog mStartTimePickerDialog;
private TimePickerDialog mEndTimePickerDialog;
private DatePickerDialog mDatePickerDialog;
/**
* Contents of the "minutes" spinner. This has default values from the XML file, augmented
* with any additional values that were already associated with the event.
*/
private ArrayList<Integer> mReminderMinuteValues;
private ArrayList<String> mReminderMinuteLabels;
/**
* Contents of the "methods" spinner. The "values" list specifies the method constant
* (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that
* aren't allowed by the Calendar will be removed.
*/
private ArrayList<Integer> mReminderMethodValues;
private ArrayList<String> mReminderMethodLabels;
/**
* Contents of the "availability" spinner. The "values" list specifies the
* type constant (e.g. {@link Events#AVAILABILITY_BUSY}) associated with the
* labels. Any types that aren't allowed by the Calendar will be removed.
*/
private ArrayList<Integer> mAvailabilityValues;
private ArrayList<String> mAvailabilityLabels;
private ArrayList<String> mOriginalAvailabilityLabels;
private ArrayAdapter<String> mAvailabilityAdapter;
private boolean mAvailabilityExplicitlySet;
private boolean mAllDayChangingAvailability;
private int mAvailabilityCurrentlySelected;
private int mDefaultReminderMinutes;
private boolean mSaveAfterQueryComplete = false;
private TimeZonePickerUtils mTzPickerUtils;
private Time mStartTime;
private Time mEndTime;
private String mTimezone;
private boolean mAllDay = false;
private int mModification = EditEventHelper.MODIFY_UNINITIALIZED;
private EventRecurrence mEventRecurrence = new EventRecurrence();
private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
private ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>();
private String mRrule;
private static StringBuilder mSB = new StringBuilder(50);
private static Formatter mF = new Formatter(mSB, Locale.getDefault());
/* This class is used to update the time buttons. */
private class TimeListener implements OnTimeSetListener {
private View mView;
public TimeListener(View view) {
mView = view;
}
@Override
public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) {
// Cache the member variables locally to avoid inner class overhead.
Time startTime = mStartTime;
Time endTime = mEndTime;
// Cache the start and end millis so that we limit the number
// of calls to normalize() and toMillis(), which are fairly
// expensive.
long startMillis;
long endMillis;
if (mView == mStartTimeButton) {
// The start time was changed.
int hourDuration = endTime.hour - startTime.hour;
int minuteDuration = endTime.minute - startTime.minute;
startTime.hour = hourOfDay;
startTime.minute = minute;
startMillis = startTime.normalize(true);
// Also update the end time to keep the duration constant.
endTime.hour = hourOfDay + hourDuration;
endTime.minute = minute + minuteDuration;
// Update tz in case the start time switched from/to DLS
populateTimezone(startMillis);
} else {
// The end time was changed.
startMillis = startTime.toMillis(true);
endTime.hour = hourOfDay;
endTime.minute = minute;
// Move to the start time if the end time is before the start
// time.
if (endTime.before(startTime)) {
endTime.monthDay = startTime.monthDay + 1;
}
// Call populateTimezone if we support end time zone as well
}
endMillis = endTime.normalize(true);
setDate(mEndDateButton, endMillis);
setTime(mStartTimeButton, startMillis);
setTime(mEndTimeButton, endMillis);
updateHomeTime();
}
}
private class TimeClickListener implements View.OnClickListener {
private Time mTime;
public TimeClickListener(Time time) {
mTime = time;
}
@Override
public void onClick(View v) {
TimePickerDialog dialog;
if (v == mStartTimeButton) {
mTimeSelectedWasStartTime = true;
if (mStartTimePickerDialog == null) {
mStartTimePickerDialog = TimePickerDialog.newInstance(new TimeListener(v),
mTime.hour, mTime.minute, DateFormat.is24HourFormat(mActivity));
} else {
mStartTimePickerDialog.setStartTime(mTime.hour, mTime.minute);
}
dialog = mStartTimePickerDialog;
} else {
mTimeSelectedWasStartTime = false;
if (mEndTimePickerDialog == null) {
mEndTimePickerDialog = TimePickerDialog.newInstance(new TimeListener(v),
mTime.hour, mTime.minute, DateFormat.is24HourFormat(mActivity));
} else {
mEndTimePickerDialog.setStartTime(mTime.hour, mTime.minute);
}
dialog = mEndTimePickerDialog;
}
final FragmentManager fm = mActivity.getFragmentManager();
fm.executePendingTransactions();
if (dialog != null && !dialog.isAdded()) {
dialog.show(fm, FRAG_TAG_TIME_PICKER);
}
}
}
private class DateListener implements OnDateSetListener {
View mView;
public DateListener(View view) {
mView = view;
}
@Override
public void onDateSet(DatePickerDialog view, int year, int month, int monthDay) {
Log.d(TAG, "onDateSet: " + year + " " + month + " " + monthDay);
// Cache the member variables locally to avoid inner class overhead.
Time startTime = mStartTime;
Time endTime = mEndTime;
// Cache the start and end millis so that we limit the number
// of calls to normalize() and toMillis(), which are fairly
// expensive.
long startMillis;
long endMillis;
if (mView == mStartDateButton) {
// The start date was changed.
int yearDuration = endTime.year - startTime.year;
int monthDuration = endTime.month - startTime.month;
int monthDayDuration = endTime.monthDay - startTime.monthDay;
startTime.year = year;
startTime.month = month;
startTime.monthDay = monthDay;
startMillis = startTime.normalize(true);
// Also update the end date to keep the duration constant.
endTime.year = year + yearDuration;
endTime.month = month + monthDuration;
endTime.monthDay = monthDay + monthDayDuration;
endMillis = endTime.normalize(true);
// If the start date has changed then update the repeats.
populateRepeats();
// Update tz in case the start time switched from/to DLS
populateTimezone(startMillis);
} else {
// The end date was changed.
startMillis = startTime.toMillis(true);
endTime.year = year;
endTime.month = month;
endTime.monthDay = monthDay;
endMillis = endTime.normalize(true);
// Do not allow an event to have an end time before the start
// time.
if (endTime.before(startTime)) {
endTime.set(startTime);
endMillis = startMillis;
}
// Call populateTimezone if we support end time zone as well
}
setDate(mStartDateButton, startMillis);
setDate(mEndDateButton, endMillis);
setTime(mEndTimeButton, endMillis); // In case end time had to be
// reset
updateHomeTime();
}
}
// Fills in the date and time fields
private void populateWhen() {
long startMillis = mStartTime.toMillis(false /* use isDst */);
long endMillis = mEndTime.toMillis(false /* use isDst */);
setDate(mStartDateButton, startMillis);
setDate(mEndDateButton, endMillis);
setTime(mStartTimeButton, startMillis);
setTime(mEndTimeButton, endMillis);
mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
}
// Implements OnTimeZoneSetListener
@Override
public void onTimeZoneSet(TimeZoneInfo tzi) {
setTimezone(tzi.mTzId);
updateHomeTime();
}
private void setTimezone(String timeZone) {
mTimezone = timeZone;
mStartTime.timezone = mTimezone;
long timeMillis = mStartTime.normalize(true);
mEndTime.timezone = mTimezone;
mEndTime.normalize(true);
populateTimezone(timeMillis);
}
private void populateTimezone(long eventStartTime) {
if (mTzPickerUtils == null) {
mTzPickerUtils = new TimeZonePickerUtils(mActivity);
}
CharSequence displayName =
mTzPickerUtils.getGmtDisplayName(mActivity, mTimezone, eventStartTime, true);
mTimezoneTextView.setText(displayName);
mTimezoneButton.setText(displayName);
}
private void showTimezoneDialog() {
Bundle b = new Bundle();
b.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, mStartTime.toMillis(false));
b.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, mTimezone);
FragmentManager fm = mActivity.getFragmentManager();
TimeZonePickerDialog tzpd = (TimeZonePickerDialog) fm
.findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER);
if (tzpd != null) {
tzpd.dismiss();
}
tzpd = new TimeZonePickerDialog();
tzpd.setArguments(b);
tzpd.setOnTimeZoneSetListener(EditEventView.this);
tzpd.show(fm, FRAG_TAG_TIME_ZONE_PICKER);
}
private void populateRepeats() {
Resources r = mActivity.getResources();
String repeatString;
boolean enabled;
if (!TextUtils.isEmpty(mRrule)) {
repeatString = EventRecurrenceFormatter.getRepeatString(mActivity, r,
mEventRecurrence, true);
if (repeatString == null) {
repeatString = r.getString(R.string.custom);
Log.e(TAG, "Can't generate display string for " + mRrule);
enabled = false;
} else {
// TODO Should give option to clear/reset rrule
enabled = RecurrencePickerDialog.canHandleRecurrenceRule(mEventRecurrence);
if (!enabled) {
Log.e(TAG, "UI can't handle " + mRrule);
}
}
} else {
repeatString = r.getString(R.string.does_not_repeat);
enabled = true;
}
mRruleButton.setText(repeatString);
// Don't allow the user to make exceptions recurring events.
if (mModel.mOriginalSyncId != null) {
enabled = false;
}
mRruleButton.setOnClickListener(this);
mRruleButton.setEnabled(enabled);
}
private class DateClickListener implements View.OnClickListener {
private Time mTime;
public DateClickListener(Time time) {
mTime = time;
}
@Override
public void onClick(View v) {
if (!mView.hasWindowFocus()) {
// Don't do anything if the activity if paused. Since Activity doesn't
// have a built in way to do this, we would have to implement one ourselves and
// either cast our Activity to a specialized activity base class or implement some
// generic interface that tells us if an activity is paused. hasWindowFocus() is
// close enough if not quite perfect.
return;
}
if (v == mStartDateButton) {
mDateSelectedWasStartDate = true;
} else {
mDateSelectedWasStartDate = false;
}
final DateListener listener = new DateListener(v);
if (mDatePickerDialog != null) {
mDatePickerDialog.dismiss();
}
mDatePickerDialog = DatePickerDialog.newInstance(listener,
mTime.year, mTime.month, mTime.monthDay);
mDatePickerDialog.setFirstDayOfWeek(Utils.getFirstDayOfWeekAsCalendar(mActivity));
mDatePickerDialog.setYearRange(Utils.YEAR_MIN, Utils.YEAR_MAX);
mDatePickerDialog.show(mActivity.getFragmentManager(), FRAG_TAG_DATE_PICKER);
}
}
public static class CalendarsAdapter extends ResourceCursorAdapter {
public CalendarsAdapter(Context context, int resourceId, Cursor c) {
super(context, resourceId, c);
setDropDownViewResource(R.layout.calendars_dropdown_item);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
View colorBar = view.findViewById(R.id.color);
int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
int nameColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_DISPLAY_NAME);
int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
if (colorBar != null) {
colorBar.setBackgroundColor(Utils.getDisplayColorFromColor(cursor
.getInt(colorColumn)));
}
TextView name = (TextView) view.findViewById(R.id.calendar_name);
if (name != null) {
String displayName = cursor.getString(nameColumn);
name.setText(displayName);
TextView accountName = (TextView) view.findViewById(R.id.account_name);
if (accountName != null) {
accountName.setText(cursor.getString(ownerColumn));
accountName.setVisibility(TextView.VISIBLE);
}
}
}
}
/**
* Does prep steps for saving a calendar event.
*
* This triggers a parse of the attendees list and checks if the event is
* ready to be saved. An event is ready to be saved so long as a model
* exists and has a calendar it can be associated with, either because it's
* an existing event or we've finished querying.
*
* @return false if there is no model or no calendar had been loaded yet,
* true otherwise.
*/
public boolean prepareForSave() {
if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) {
return false;
}
return fillModelFromUI();
}
public boolean fillModelFromReadOnlyUi() {
if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) {
return false;
}
mModel.mReminders = EventViewUtils.reminderItemsToReminders(
mReminderItems, mReminderMinuteValues, mReminderMethodValues);
mModel.mReminders.addAll(mUnsupportedReminders);
mModel.normalizeReminders();
int status = EventInfoFragment.getResponseFromButtonId(
mResponseRadioGroup.getCheckedRadioButtonId());
if (status != Attendees.ATTENDEE_STATUS_NONE) {
mModel.mSelfAttendeeStatus = status;
}
return true;
}
// This is called if the user clicks on one of the buttons: "Save",
// "Discard", or "Delete". This is also called if the user clicks
// on the "remove reminder" button.
@Override
public void onClick(View view) {
if (view == mRruleButton) {
Bundle b = new Bundle();
b.putLong(RecurrencePickerDialog.BUNDLE_START_TIME_MILLIS,
mStartTime.toMillis(false));
b.putString(RecurrencePickerDialog.BUNDLE_TIME_ZONE, mStartTime.timezone);
// TODO may be more efficient to serialize and pass in EventRecurrence
b.putString(RecurrencePickerDialog.BUNDLE_RRULE, mRrule);
FragmentManager fm = mActivity.getFragmentManager();
RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm
.findFragmentByTag(FRAG_TAG_RECUR_PICKER);
if (rpd != null) {
rpd.dismiss();
}
rpd = new RecurrencePickerDialog();
rpd.setArguments(b);
rpd.setOnRecurrenceSetListener(EditEventView.this);
rpd.show(fm, FRAG_TAG_RECUR_PICKER);
return;
}
// This must be a click on one of the "remove reminder" buttons
LinearLayout reminderItem = (LinearLayout) view.getParent();
LinearLayout parent = (LinearLayout) reminderItem.getParent();
parent.removeView(reminderItem);
mReminderItems.remove(reminderItem);
updateRemindersVisibility(mReminderItems.size());
EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders);
}
@Override
public void onRecurrenceSet(String rrule) {
Log.d(TAG, "Old rrule:" + mRrule);
Log.d(TAG, "New rrule:" + rrule);
mRrule = rrule;
if (mRrule != null) {
mEventRecurrence.parse(mRrule);
}
populateRepeats();
}
// This is called if the user cancels the "No calendars" dialog.
// The "No calendars" dialog is shown if there are no syncable calendars.
@Override
public void onCancel(DialogInterface dialog) {
if (dialog == mLoadingCalendarsDialog) {
mLoadingCalendarsDialog = null;
mSaveAfterQueryComplete = false;
} else if (dialog == mNoCalendarsDialog) {
mDone.setDoneCode(Utils.DONE_REVERT);
mDone.run();
return;
}
}
// This is called if the user clicks on a dialog button.
@Override
public void onClick(DialogInterface dialog, int which) {
if (dialog == mNoCalendarsDialog) {
mDone.setDoneCode(Utils.DONE_REVERT);
mDone.run();
if (which == DialogInterface.BUTTON_POSITIVE) {
Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT);
final String[] array = {"com.android.calendar"};
nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array);
nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity.startActivity(nextIntent);
}
}
}
// Goes through the UI elements and updates the model as necessary
private boolean fillModelFromUI() {
if (mModel == null) {
return false;
}
mModel.mReminders = EventViewUtils.reminderItemsToReminders(mReminderItems,
mReminderMinuteValues, mReminderMethodValues);
mModel.mReminders.addAll(mUnsupportedReminders);
mModel.normalizeReminders();
mModel.mHasAlarm = mReminderItems.size() > 0;
mModel.mTitle = mTitleTextView.getText().toString();
mModel.mAllDay = mAllDayCheckBox.isChecked();
mModel.mLocation = mLocationTextView.getText().toString();
mModel.mDescription = mDescriptionTextView.getText().toString();
if (TextUtils.isEmpty(mModel.mLocation)) {
mModel.mLocation = null;
}
if (TextUtils.isEmpty(mModel.mDescription)) {
mModel.mDescription = null;
}
int status = EventInfoFragment.getResponseFromButtonId(mResponseRadioGroup
.getCheckedRadioButtonId());
if (status != Attendees.ATTENDEE_STATUS_NONE) {
mModel.mSelfAttendeeStatus = status;
}
if (mAttendeesList != null) {
mEmailValidator.setRemoveInvalid(true);
mAttendeesList.performValidation();
mModel.mAttendeesList.clear();
mModel.addAttendees(mAttendeesList.getText().toString(), mEmailValidator);
mEmailValidator.setRemoveInvalid(false);
}
// If this was a new event we need to fill in the Calendar information
if (mModel.mUri == null) {
mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
String defaultCalendar = mCalendarsCursor.getString(
EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
Utils.setSharedPreference(
mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar);
mModel.mOwnerAccount = defaultCalendar;
mModel.mOrganizer = defaultCalendar;
mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
}
}
if (mModel.mAllDay) {
// Reset start and end time, increment the monthDay by 1, and set
// the timezone to UTC, as required for all-day events.
mTimezone = Time.TIMEZONE_UTC;
mStartTime.hour = 0;
mStartTime.minute = 0;
mStartTime.second = 0;
mStartTime.timezone = mTimezone;
mModel.mStart = mStartTime.normalize(true);
mEndTime.hour = 0;
mEndTime.minute = 0;
mEndTime.second = 0;
mEndTime.timezone = mTimezone;
// When a user see the event duration as "X - Y" (e.g. Oct. 28 - Oct. 29), end time
// should be Y + 1 (Oct.30).
final long normalizedEndTimeMillis =
mEndTime.normalize(true) + DateUtils.DAY_IN_MILLIS;
if (normalizedEndTimeMillis < mModel.mStart) {
// mEnd should be midnight of the next day of mStart.
mModel.mEnd = mModel.mStart + DateUtils.DAY_IN_MILLIS;
} else {
mModel.mEnd = normalizedEndTimeMillis;
}
} else {
mStartTime.timezone = mTimezone;
mEndTime.timezone = mTimezone;
mModel.mStart = mStartTime.toMillis(true);
mModel.mEnd = mEndTime.toMillis(true);
}
mModel.mTimezone = mTimezone;
mModel.mAccessLevel = mAccessLevelSpinner.getSelectedItemPosition();
// TODO set correct availability value
mModel.mAvailability = mAvailabilityValues.get(mAvailabilitySpinner
.getSelectedItemPosition());
// rrrule
// If we're making an exception we don't want it to be a repeating
// event.
if (mModification == EditEventHelper.MODIFY_SELECTED) {
mModel.mRrule = null;
} else {
mModel.mRrule = mRrule;
}
return true;
}
public EditEventView(Activity activity, View view, EditDoneRunnable done,
boolean timeSelectedWasStartTime, boolean dateSelectedWasStartDate) {
mActivity = activity;
mView = view;
mDone = done;
// cache top level view elements
mLoadingMessage = (TextView) view.findViewById(R.id.loading_message);
mScrollView = (ScrollView) view.findViewById(R.id.scroll_view);
// cache all the widgets
mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner);
mTitleTextView = (TextView) view.findViewById(R.id.title);
mLocationTextView = (AutoCompleteTextView) view.findViewById(R.id.location);
mDescriptionTextView = (TextView) view.findViewById(R.id.description);
mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label);
mStartDateButton = (Button) view.findViewById(R.id.start_date);
mEndDateButton = (Button) view.findViewById(R.id.end_date);
mWhenView = (TextView) mView.findViewById(R.id.when);
mTimezoneTextView = (TextView) mView.findViewById(R.id.timezone_textView);
mStartTimeButton = (Button) view.findViewById(R.id.start_time);
mEndTimeButton = (Button) view.findViewById(R.id.end_time);
mTimezoneButton = (Button) view.findViewById(R.id.timezone_button);
mTimezoneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showTimezoneDialog();
}
});
mTimezoneRow = view.findViewById(R.id.timezone_button_row);
mStartTimeHome = (TextView) view.findViewById(R.id.start_time_home_tz);
mStartDateHome = (TextView) view.findViewById(R.id.start_date_home_tz);
mEndTimeHome = (TextView) view.findViewById(R.id.end_time_home_tz);
mEndDateHome = (TextView) view.findViewById(R.id.end_date_home_tz);
mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day);
mRruleButton = (Button) view.findViewById(R.id.rrule);
mAvailabilitySpinner = (Spinner) view.findViewById(R.id.availability);
mAccessLevelSpinner = (Spinner) view.findViewById(R.id.visibility);
mCalendarSelectorGroup = view.findViewById(R.id.calendar_selector_group);
mCalendarSelectorWrapper = view.findViewById(R.id.calendar_selector_wrapper);
mCalendarStaticGroup = view.findViewById(R.id.calendar_group);
mRemindersGroup = view.findViewById(R.id.reminders_row);
mResponseGroup = view.findViewById(R.id.response_row);
mOrganizerGroup = view.findViewById(R.id.organizer_row);
mAttendeesGroup = view.findViewById(R.id.add_attendees_row);
mLocationGroup = view.findViewById(R.id.where_row);
mDescriptionGroup = view.findViewById(R.id.description_row);
mStartHomeGroup = view.findViewById(R.id.from_row_home_tz);
mEndHomeGroup = view.findViewById(R.id.to_row_home_tz);
mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees);
mColorPickerNewEvent = view.findViewById(R.id.change_color_new_event);
mColorPickerExistingEvent = view.findViewById(R.id.change_color_existing_event);
mTitleTextView.setTag(mTitleTextView.getBackground());
mLocationTextView.setTag(mLocationTextView.getBackground());
mLocationAdapter = new EventLocationAdapter(activity);
mLocationTextView.setAdapter(mLocationAdapter);
mLocationTextView.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
// Dismiss the suggestions dropdown. Return false so the other
// side effects still occur (soft keyboard going away, etc.).
mLocationTextView.dismissDropDown();
}
return false;
}
});
mAvailabilityExplicitlySet = false;
mAllDayChangingAvailability = false;
mAvailabilityCurrentlySelected = -1;
mAvailabilitySpinner.setOnItemSelectedListener(
new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent,
View view, int position, long id) {
// The spinner's onItemSelected gets called while it is being
// initialized to the first item, and when we explicitly set it
// in the allDay checkbox toggling, so we need these checks to
// find out when the spinner is actually being clicked.
// Set the initial selection.
if (mAvailabilityCurrentlySelected == -1) {
mAvailabilityCurrentlySelected = position;
}
if (mAvailabilityCurrentlySelected != position &&
!mAllDayChangingAvailability) {
mAvailabilityExplicitlySet = true;
} else {
mAvailabilityCurrentlySelected = position;
mAllDayChangingAvailability = false;
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) { }
});
mDescriptionTextView.setTag(mDescriptionTextView.getBackground());
mAttendeesList.setTag(mAttendeesList.getBackground());
mOriginalPadding[0] = mLocationTextView.getPaddingLeft();
mOriginalPadding[1] = mLocationTextView.getPaddingTop();
mOriginalPadding[2] = mLocationTextView.getPaddingRight();
mOriginalPadding[3] = mLocationTextView.getPaddingBottom();
mEditViewList.add(mTitleTextView);
mEditViewList.add(mLocationTextView);
mEditViewList.add(mDescriptionTextView);
mEditViewList.add(mAttendeesList);
mViewOnlyList.add(view.findViewById(R.id.when_row));
mViewOnlyList.add(view.findViewById(R.id.timezone_textview_row));
mEditOnlyList.add(view.findViewById(R.id.all_day_row));
mEditOnlyList.add(view.findViewById(R.id.availability_row));
mEditOnlyList.add(view.findViewById(R.id.visibility_row));
mEditOnlyList.add(view.findViewById(R.id.from_row));
mEditOnlyList.add(view.findViewById(R.id.to_row));
mEditOnlyList.add(mTimezoneRow);
mEditOnlyList.add(mStartHomeGroup);
mEditOnlyList.add(mEndHomeGroup);
mResponseRadioGroup = (RadioGroup) view.findViewById(R.id.response_value);
mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container);
mTimezone = Utils.getTimeZone(activity, null);
mIsMultipane = activity.getResources().getBoolean(R.bool.tablet_config);
mStartTime = new Time(mTimezone);
mEndTime = new Time(mTimezone);
mEmailValidator = new Rfc822Validator(null);
initMultiAutoCompleteTextView((RecipientEditTextView) mAttendeesList);
// Display loading screen
setModel(null);
FragmentManager fm = activity.getFragmentManager();
RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm
.findFragmentByTag(FRAG_TAG_RECUR_PICKER);
if (rpd != null) {
rpd.setOnRecurrenceSetListener(this);
}
TimeZonePickerDialog tzpd = (TimeZonePickerDialog) fm
.findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER);
if (tzpd != null) {
tzpd.setOnTimeZoneSetListener(this);
}
TimePickerDialog tpd = (TimePickerDialog) fm.findFragmentByTag(FRAG_TAG_TIME_PICKER);
if (tpd != null) {
View v;
mTimeSelectedWasStartTime = timeSelectedWasStartTime;
if (timeSelectedWasStartTime) {
v = mStartTimeButton;
} else {
v = mEndTimeButton;
}
tpd.setOnTimeSetListener(new TimeListener(v));
}
mDatePickerDialog = (DatePickerDialog) fm.findFragmentByTag(FRAG_TAG_DATE_PICKER);
if (mDatePickerDialog != null) {
View v;
mDateSelectedWasStartDate = dateSelectedWasStartDate;
if (dateSelectedWasStartDate) {
v = mStartDateButton;
} else {
v = mEndDateButton;
}
mDatePickerDialog.setOnDateSetListener(new DateListener(v));
}
}
/**
* Loads an integer array asset into a list.
*/
private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
int[] vals = r.getIntArray(resNum);
int size = vals.length;
ArrayList<Integer> list = new ArrayList<Integer>(size);
for (int i = 0; i < size; i++) {
list.add(vals[i]);
}
return list;
}
/**
* Loads a String array asset into a list.
*/
private static ArrayList<String> loadStringArray(Resources r, int resNum) {
String[] labels = r.getStringArray(resNum);
ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
return list;
}
private void prepareAvailability() {
Resources r = mActivity.getResources();
mAvailabilityValues = loadIntegerArray(r, R.array.availability_values);
mAvailabilityLabels = loadStringArray(r, R.array.availability);
// Copy the unadulterated availability labels for all-day toggling.
mOriginalAvailabilityLabels = new ArrayList<String>();
mOriginalAvailabilityLabels.addAll(mAvailabilityLabels);
if (mModel.mCalendarAllowedAvailability != null) {
EventViewUtils.reduceMethodList(mAvailabilityValues, mAvailabilityLabels,
mModel.mCalendarAllowedAvailability);
}
mAvailabilityAdapter = new ArrayAdapter<String>(mActivity,
android.R.layout.simple_spinner_item, mAvailabilityLabels);
mAvailabilityAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mAvailabilitySpinner.setAdapter(mAvailabilityAdapter);
}
/**
* Prepares the reminder UI elements.
* <p>
* (Re-)loads the minutes / methods lists from the XML assets, adds/removes items as
* needed for the current set of reminders and calendar properties, and then creates UI
* elements.
*/
private void prepareReminders() {
CalendarEventModel model = mModel;
Resources r = mActivity.getResources();
// Load the labels and corresponding numeric values for the minutes and methods lists
// from the assets. If we're switching calendars, we need to clear and re-populate the
// lists (which may have elements added and removed based on calendar properties). This
// is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
// new event that aren't in the default set.
mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
// Remove any reminder methods that aren't allowed for this calendar. If this is
// a new event, mCalendarAllowedReminders may not be set the first time we're called.
if (mModel.mCalendarAllowedReminders != null) {
EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
mModel.mCalendarAllowedReminders);
}
int numReminders = 0;
if (model.mHasAlarm) {
ArrayList<ReminderEntry> reminders = model.mReminders;
numReminders = reminders.size();
// Insert any minute values that aren't represented in the minutes list.
for (ReminderEntry re : reminders) {
if (mReminderMethodValues.contains(re.getMethod())) {
EventViewUtils.addMinutesToList(mActivity, mReminderMinuteValues,
mReminderMinuteLabels, re.getMinutes());
}
}
// Create a UI element for each reminder. We display all of the reminders we get
// from the provider, even if the count exceeds the calendar maximum. (Also, for
// a new event, we won't have a maxReminders value available.)
mUnsupportedReminders.clear();
for (ReminderEntry re : reminders) {
if (mReminderMethodValues.contains(re.getMethod())
|| re.getMethod() == Reminders.METHOD_DEFAULT) {
EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
mReminderMethodLabels, re, Integer.MAX_VALUE, null);
} else {
// TODO figure out a way to display unsupported reminders
mUnsupportedReminders.add(re);
}
}
}
updateRemindersVisibility(numReminders);
EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders);
}
/**
* Fill in the view with the contents of the given event model. This allows
* an edit view to be initialized before the event has been loaded. Passing
* in null for the model will display a loading screen. A non-null model
* will fill in the view's fields with the data contained in the model.
*
* @param model The event model to pull the data from
*/
public void setModel(CalendarEventModel model) {
mModel = model;
// Need to close the autocomplete adapter to prevent leaking cursors.
if (mAddressAdapter != null && mAddressAdapter instanceof EmailAddressAdapter) {
((EmailAddressAdapter)mAddressAdapter).close();
mAddressAdapter = null;
}
if (model == null) {
// Display loading screen
mLoadingMessage.setVisibility(View.VISIBLE);
mScrollView.setVisibility(View.GONE);
return;
}
boolean canRespond = EditEventHelper.canRespond(model);
long begin = model.mStart;
long end = model.mEnd;
mTimezone = model.mTimezone; // this will be UTC for all day events
// Set up the starting times
if (begin > 0) {
mStartTime.timezone = mTimezone;
mStartTime.set(begin);
mStartTime.normalize(true);
}
if (end > 0) {
mEndTime.timezone = mTimezone;
mEndTime.set(end);
mEndTime.normalize(true);
}
mRrule = model.mRrule;
if (!TextUtils.isEmpty(mRrule)) {
mEventRecurrence.parse(mRrule);
}
if (mEventRecurrence.startDate == null) {
mEventRecurrence.startDate = mStartTime;
}
// If the user is allowed to change the attendees set up the view and
// validator
if (!model.mHasAttendeeData) {
mAttendeesGroup.setVisibility(View.GONE);
}
mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
setAllDayViewsVisibility(isChecked);
}
});
boolean prevAllDay = mAllDayCheckBox.isChecked();
mAllDay = false; // default to false. Let setAllDayViewsVisibility update it as needed
if (model.mAllDay) {
mAllDayCheckBox.setChecked(true);
// put things back in local time for all day events
mTimezone = Utils.getTimeZone(mActivity, null);
mStartTime.timezone = mTimezone;
mEndTime.timezone = mTimezone;
mEndTime.normalize(true);
} else {
mAllDayCheckBox.setChecked(false);
}
// On a rotation we need to update the views but onCheckedChanged
// doesn't get called
if (prevAllDay == mAllDayCheckBox.isChecked()) {
setAllDayViewsVisibility(prevAllDay);
}
populateTimezone(mStartTime.normalize(true));
SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
String defaultReminderString = prefs.getString(
GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
prepareReminders();
prepareAvailability();
View reminderAddButton = mView.findViewById(R.id.reminder_add);
View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
addReminder();
}
};
reminderAddButton.setOnClickListener(addReminderOnClickListener);
if (!mIsMultipane) {
mView.findViewById(R.id.is_all_day_label).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
mAllDayCheckBox.setChecked(!mAllDayCheckBox.isChecked());
}
});
}
if (model.mTitle != null) {
mTitleTextView.setTextKeepState(model.mTitle);
}
if (model.mIsOrganizer || TextUtils.isEmpty(model.mOrganizer)
|| model.mOrganizer.endsWith(GOOGLE_SECONDARY_CALENDAR)) {
mView.findViewById(R.id.organizer_label).setVisibility(View.GONE);
mView.findViewById(R.id.organizer).setVisibility(View.GONE);
mOrganizerGroup.setVisibility(View.GONE);
} else {
((TextView) mView.findViewById(R.id.organizer)).setText(model.mOrganizerDisplayName);
}
if (model.mLocation != null) {
mLocationTextView.setTextKeepState(model.mLocation);
}
if (model.mDescription != null) {
mDescriptionTextView.setTextKeepState(model.mDescription);
}
int availIndex = mAvailabilityValues.indexOf(model.mAvailability);
if (availIndex != -1) {
mAvailabilitySpinner.setSelection(availIndex);
}
mAccessLevelSpinner.setSelection(model.mAccessLevel);
View responseLabel = mView.findViewById(R.id.response_label);
if (canRespond) {
int buttonToCheck = EventInfoFragment
.findButtonIdForResponse(model.mSelfAttendeeStatus);
mResponseRadioGroup.check(buttonToCheck); // -1 clear all radio buttons
mResponseRadioGroup.setVisibility(View.VISIBLE);
responseLabel.setVisibility(View.VISIBLE);
} else {
responseLabel.setVisibility(View.GONE);
mResponseRadioGroup.setVisibility(View.GONE);
mResponseGroup.setVisibility(View.GONE);
}
if (model.mUri != null) {
// This is an existing event so hide the calendar spinner
// since we can't change the calendar.
View calendarGroup = mView.findViewById(R.id.calendar_selector_group);
calendarGroup.setVisibility(View.GONE);
TextView tv = (TextView) mView.findViewById(R.id.calendar_textview);
tv.setText(model.mCalendarDisplayName);
tv = (TextView) mView.findViewById(R.id.calendar_textview_secondary);
if (tv != null) {
tv.setText(model.mOwnerAccount);
}
} else {
View calendarGroup = mView.findViewById(R.id.calendar_group);
calendarGroup.setVisibility(View.GONE);
}
if (model.isEventColorInitialized()) {
updateHeadlineColor(model, model.getEventColor());
}
populateWhen();
populateRepeats();
updateAttendees(model.mAttendeesList);
updateView();
mScrollView.setVisibility(View.VISIBLE);
mLoadingMessage.setVisibility(View.GONE);
sendAccessibilityEvent();
}
public void updateHeadlineColor(CalendarEventModel model, int displayColor) {
if (model.mUri != null) {
if (mIsMultipane) {
mView.findViewById(R.id.calendar_textview_with_colorpicker)
.setBackgroundColor(displayColor);
} else {
mView.findViewById(R.id.calendar_group).setBackgroundColor(displayColor);
}
} else {
setSpinnerBackgroundColor(displayColor);
}
}
private void setSpinnerBackgroundColor(int displayColor) {
if (mIsMultipane) {
mCalendarSelectorWrapper.setBackgroundColor(displayColor);
} else {
mCalendarSelectorGroup.setBackgroundColor(displayColor);
}
}
private void sendAccessibilityEvent() {
AccessibilityManager am =
(AccessibilityManager) mActivity.getSystemService(Service.ACCESSIBILITY_SERVICE);
if (!am.isEnabled() || mModel == null) {
return;
}
StringBuilder b = new StringBuilder();
addFieldsRecursive(b, mView);
CharSequence msg = b.toString();
AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
event.setClassName(getClass().getName());
event.setPackageName(mActivity.getPackageName());
event.getText().add(msg);
event.setAddedCount(msg.length());
am.sendAccessibilityEvent(event);
}
private void addFieldsRecursive(StringBuilder b, View v) {
if (v == null || v.getVisibility() != View.VISIBLE) {
return;
}
if (v instanceof TextView) {
CharSequence tv = ((TextView) v).getText();
if (!TextUtils.isEmpty(tv.toString().trim())) {
b.append(tv + PERIOD_SPACE);
}
} else if (v instanceof RadioGroup) {
RadioGroup rg = (RadioGroup) v;
int id = rg.getCheckedRadioButtonId();
if (id != View.NO_ID) {
b.append(((RadioButton) (v.findViewById(id))).getText() + PERIOD_SPACE);
}
} else if (v instanceof Spinner) {
Spinner s = (Spinner) v;
if (s.getSelectedItem() instanceof String) {
String str = ((String) (s.getSelectedItem())).trim();
if (!TextUtils.isEmpty(str)) {
b.append(str + PERIOD_SPACE);
}
}
} else if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
int children = vg.getChildCount();
for (int i = 0; i < children; i++) {
addFieldsRecursive(b, vg.getChildAt(i));
}
}
}
/**
* Creates a single line string for the time/duration
*/
protected void setWhenString() {
String when;
int flags = DateUtils.FORMAT_SHOW_DATE;
String tz = mTimezone;
if (mModel.mAllDay) {
flags |= DateUtils.FORMAT_SHOW_WEEKDAY;
tz = Time.TIMEZONE_UTC;
} else {
flags |= DateUtils.FORMAT_SHOW_TIME;
if (DateFormat.is24HourFormat(mActivity)) {
flags |= DateUtils.FORMAT_24HOUR;
}
}
long startMillis = mStartTime.normalize(true);
long endMillis = mEndTime.normalize(true);
mSB.setLength(0);
when = DateUtils
.formatDateRange(mActivity, mF, startMillis, endMillis, flags, tz).toString();
mWhenView.setText(when);
}
/**
* Configures the Calendars spinner. This is only done for new events, because only new
* events allow you to select a calendar while editing an event.
* <p>
* We tuck a reference to a Cursor with calendar database data into the spinner, so that
* we can easily extract calendar-specific values when the value changes (the spinner's
* onItemSelected callback is configured).
*/
public void setCalendarsCursor(Cursor cursor, boolean userVisible, long selectedCalendarId) {
// If there are no syncable calendars, then we cannot allow
// creating a new event.
mCalendarsCursor = cursor;
if (cursor == null || cursor.getCount() == 0) {
// Cancel the "loading calendars" dialog if it exists
if (mSaveAfterQueryComplete) {
mLoadingCalendarsDialog.cancel();
}
if (!userVisible) {
return;
}
// Create an error message for the user that, when clicked,
// will exit this activity without saving the event.
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(R.string.no_syncable_calendars).setIconAttribute(
android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found)
.setPositiveButton(R.string.add_account, this)
.setNegativeButton(android.R.string.no, this).setOnCancelListener(this);
mNoCalendarsDialog = builder.show();
return;
}
int selection;
if (selectedCalendarId != -1) {
selection = findSelectedCalendarPosition(cursor, selectedCalendarId);
} else {
selection = findDefaultCalendarPosition(cursor);
}
// populate the calendars spinner
CalendarsAdapter adapter = new CalendarsAdapter(mActivity,
R.layout.calendars_spinner_item, cursor);
mCalendarsSpinner.setAdapter(adapter);
mCalendarsSpinner.setOnItemSelectedListener(this);
mCalendarsSpinner.setSelection(selection);
if (mSaveAfterQueryComplete) {
mLoadingCalendarsDialog.cancel();
if (prepareForSave() && fillModelFromUI()) {
int exit = userVisible ? Utils.DONE_EXIT : 0;
mDone.setDoneCode(Utils.DONE_SAVE | exit);
mDone.run();
} else if (userVisible) {
mDone.setDoneCode(Utils.DONE_EXIT);
mDone.run();
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view");
}
return;
}
}
/**
* Updates the view based on {@link #mModification} and {@link #mModel}
*/
public void updateView() {
if (mModel == null) {
return;
}
if (EditEventHelper.canModifyEvent(mModel)) {
setViewStates(mModification);
} else {
setViewStates(Utils.MODIFY_UNINITIALIZED);
}
}
private void setViewStates(int mode) {
// Extra canModify check just in case
if (mode == Utils.MODIFY_UNINITIALIZED || !EditEventHelper.canModifyEvent(mModel)) {
setWhenString();
for (View v : mViewOnlyList) {
v.setVisibility(View.VISIBLE);
}
for (View v : mEditOnlyList) {
v.setVisibility(View.GONE);
}
for (View v : mEditViewList) {
v.setEnabled(false);
v.setBackgroundDrawable(null);
}
mCalendarSelectorGroup.setVisibility(View.GONE);
mCalendarStaticGroup.setVisibility(View.VISIBLE);
mRruleButton.setEnabled(false);
if (EditEventHelper.canAddReminders(mModel)) {
mRemindersGroup.setVisibility(View.VISIBLE);
} else {
mRemindersGroup.setVisibility(View.GONE);
}
if (TextUtils.isEmpty(mLocationTextView.getText())) {
mLocationGroup.setVisibility(View.GONE);
}
if (TextUtils.isEmpty(mDescriptionTextView.getText())) {
mDescriptionGroup.setVisibility(View.GONE);
}
} else {
for (View v : mViewOnlyList) {
v.setVisibility(View.GONE);
}
for (View v : mEditOnlyList) {
v.setVisibility(View.VISIBLE);
}
for (View v : mEditViewList) {
v.setEnabled(true);
if (v.getTag() != null) {
v.setBackgroundDrawable((Drawable) v.getTag());
v.setPadding(mOriginalPadding[0], mOriginalPadding[1], mOriginalPadding[2],
mOriginalPadding[3]);
}
}
if (mModel.mUri == null) {
mCalendarSelectorGroup.setVisibility(View.VISIBLE);
mCalendarStaticGroup.setVisibility(View.GONE);
} else {
mCalendarSelectorGroup.setVisibility(View.GONE);
mCalendarStaticGroup.setVisibility(View.VISIBLE);
}
if (mModel.mOriginalSyncId == null) {
mRruleButton.setEnabled(true);
} else {
mRruleButton.setEnabled(false);
mRruleButton.setBackgroundDrawable(null);
}
mRemindersGroup.setVisibility(View.VISIBLE);
mLocationGroup.setVisibility(View.VISIBLE);
mDescriptionGroup.setVisibility(View.VISIBLE);
}
setAllDayViewsVisibility(mAllDayCheckBox.isChecked());
}
public void setModification(int modifyWhich) {
mModification = modifyWhich;
updateView();
updateHomeTime();
}
private int findSelectedCalendarPosition(Cursor calendarsCursor, long calendarId) {
if (calendarsCursor.getCount() <= 0) {
return -1;
}
int calendarIdColumn = calendarsCursor.getColumnIndexOrThrow(Calendars._ID);
int position = 0;
calendarsCursor.moveToPosition(-1);
while (calendarsCursor.moveToNext()) {
if (calendarsCursor.getLong(calendarIdColumn) == calendarId) {
return position;
}
position++;
}
return 0;
}
// Find the calendar position in the cursor that matches calendar in
// preference
private int findDefaultCalendarPosition(Cursor calendarsCursor) {
if (calendarsCursor.getCount() <= 0) {
return -1;
}
String defaultCalendar = Utils.getSharedPreference(
mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, (String) null);
int calendarsOwnerIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
int accountNameIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME);
int accountTypeIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE);
int position = 0;
calendarsCursor.moveToPosition(-1);
while (calendarsCursor.moveToNext()) {
String calendarOwner = calendarsCursor.getString(calendarsOwnerIndex);
if (defaultCalendar == null) {
// There is no stored default upon the first time running. Use a primary
// calendar in this case.
if (calendarOwner != null &&
calendarOwner.equals(calendarsCursor.getString(accountNameIndex)) &&
!CalendarContract.ACCOUNT_TYPE_LOCAL.equals(
calendarsCursor.getString(accountTypeIndex))) {
return position;
}
} else if (defaultCalendar.equals(calendarOwner)) {
// Found the default calendar.
return position;
}
position++;
}
return 0;
}
private void updateAttendees(HashMap<String, Attendee> attendeesList) {
if (attendeesList == null || attendeesList.isEmpty()) {
return;
}
mAttendeesList.setText(null);
for (Attendee attendee : attendeesList.values()) {
// TODO: Please remove separator when Calendar uses the chips MR2 project
// Adding a comma separator between email addresses to prevent a chips MR1.1 bug
// in which email addresses are concatenated together with no separator.
mAttendeesList.append(attendee.mEmail + ", ");
}
}
private void updateRemindersVisibility(int numReminders) {
if (numReminders == 0) {
mRemindersContainer.setVisibility(View.GONE);
} else {
mRemindersContainer.setVisibility(View.VISIBLE);
}
}
/**
* Add a new reminder when the user hits the "add reminder" button. We use the default
* reminder time and method.
*/
private void addReminder() {
// TODO: when adding a new reminder, make it different from the
// last one in the list (if any).
if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
mReminderMinuteValues, mReminderMinuteLabels,
mReminderMethodValues, mReminderMethodLabels,
ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME),
mModel.mCalendarMaxReminders, null);
} else {
EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
mReminderMinuteValues, mReminderMinuteLabels,
mReminderMethodValues, mReminderMethodLabels,
ReminderEntry.valueOf(mDefaultReminderMinutes),
mModel.mCalendarMaxReminders, null);
}
updateRemindersVisibility(mReminderItems.size());
EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders);
}
// From com.google.android.gm.ComposeActivity
private MultiAutoCompleteTextView initMultiAutoCompleteTextView(RecipientEditTextView list) {
if (ChipsUtil.supportsChipsUi()) {
mAddressAdapter = new RecipientAdapter(mActivity);
list.setAdapter((BaseRecipientAdapter) mAddressAdapter);
list.setOnFocusListShrinkRecipients(false);
} else {
mAddressAdapter = new EmailAddressAdapter(mActivity);
list.setAdapter((EmailAddressAdapter)mAddressAdapter);
}
list.setTokenizer(new Rfc822Tokenizer());
list.setValidator(mEmailValidator);
// NOTE: assumes no other filters are set
list.setFilters(sRecipientFilters);
return list;
}
/**
* From com.google.android.gm.ComposeActivity Implements special address
* cleanup rules: The first space key entry following an "@" symbol that is
* followed by any combination of letters and symbols, including one+ dots
* and zero commas, should insert an extra comma (followed by the space).
*/
private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
private void setDate(TextView view, long millis) {
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH
| DateUtils.FORMAT_ABBREV_WEEKDAY;
// Unfortunately, DateUtils doesn't support a timezone other than the
// default timezone provided by the system, so we have this ugly hack
// here to trick it into formatting our time correctly. In order to
// prevent all sorts of craziness, we synchronize on the TimeZone class
// to prevent other threads from reading an incorrect timezone from
// calls to TimeZone#getDefault()
// TODO fix this if/when DateUtils allows for passing in a timezone
String dateString;
synchronized (TimeZone.class) {
TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
dateString = DateUtils.formatDateTime(mActivity, millis, flags);
// setting the default back to null restores the correct behavior
TimeZone.setDefault(null);
}
view.setText(dateString);
}
private void setTime(TextView view, long millis) {
int flags = DateUtils.FORMAT_SHOW_TIME;
flags |= DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
if (DateFormat.is24HourFormat(mActivity)) {
flags |= DateUtils.FORMAT_24HOUR;
}
// Unfortunately, DateUtils doesn't support a timezone other than the
// default timezone provided by the system, so we have this ugly hack
// here to trick it into formatting our time correctly. In order to
// prevent all sorts of craziness, we synchronize on the TimeZone class
// to prevent other threads from reading an incorrect timezone from
// calls to TimeZone#getDefault()
// TODO fix this if/when DateUtils allows for passing in a timezone
String timeString;
synchronized (TimeZone.class) {
TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
timeString = DateUtils.formatDateTime(mActivity, millis, flags);
TimeZone.setDefault(null);
}
view.setText(timeString);
}
/**
* @param isChecked
*/
protected void setAllDayViewsVisibility(boolean isChecked) {
if (isChecked) {
if (mEndTime.hour == 0 && mEndTime.minute == 0) {
if (mAllDay != isChecked) {
mEndTime.monthDay--;
}
long endMillis = mEndTime.normalize(true);
// Do not allow an event to have an end time
// before the
// start time.
if (mEndTime.before(mStartTime)) {
mEndTime.set(mStartTime);
endMillis = mEndTime.normalize(true);
}
setDate(mEndDateButton, endMillis);
setTime(mEndTimeButton, endMillis);
}
mStartTimeButton.setVisibility(View.GONE);
mEndTimeButton.setVisibility(View.GONE);
mTimezoneRow.setVisibility(View.GONE);
} else {
if (mEndTime.hour == 0 && mEndTime.minute == 0) {
if (mAllDay != isChecked) {
mEndTime.monthDay++;
}
long endMillis = mEndTime.normalize(true);
setDate(mEndDateButton, endMillis);
setTime(mEndTimeButton, endMillis);
}
mStartTimeButton.setVisibility(View.VISIBLE);
mEndTimeButton.setVisibility(View.VISIBLE);
mTimezoneRow.setVisibility(View.VISIBLE);
}
// If this is a new event, and if availability has not yet been
// explicitly set, toggle busy/available as the inverse of all day.
if (mModel.mUri == null && !mAvailabilityExplicitlySet) {
// Values are from R.arrays.availability_values.
// 0 = busy
// 1 = available
int newAvailabilityValue = isChecked? 1 : 0;
if (mAvailabilityAdapter != null && mAvailabilityValues != null
&& mAvailabilityValues.contains(newAvailabilityValue)) {
// We'll need to let the spinner's listener know that we're
// explicitly toggling it.
mAllDayChangingAvailability = true;
String newAvailabilityLabel = mOriginalAvailabilityLabels.get(newAvailabilityValue);
int newAvailabilityPos = mAvailabilityAdapter.getPosition(newAvailabilityLabel);
mAvailabilitySpinner.setSelection(newAvailabilityPos);
}
}
mAllDay = isChecked;
updateHomeTime();
}
public void setColorPickerButtonStates(int[] colorArray) {
setColorPickerButtonStates(colorArray != null && colorArray.length > 0);
}
public void setColorPickerButtonStates(boolean showColorPalette) {
if (showColorPalette) {
mColorPickerNewEvent.setVisibility(View.VISIBLE);
mColorPickerExistingEvent.setVisibility(View.VISIBLE);
} else {
mColorPickerNewEvent.setVisibility(View.INVISIBLE);
mColorPickerExistingEvent.setVisibility(View.GONE);
}
}
public boolean isColorPaletteVisible() {
return mColorPickerNewEvent.getVisibility() == View.VISIBLE ||
mColorPickerExistingEvent.getVisibility() == View.VISIBLE;
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// This is only used for the Calendar spinner in new events, and only fires when the
// calendar selection changes or on screen rotation
Cursor c = (Cursor) parent.getItemAtPosition(position);
if (c == null) {
// TODO: can this happen? should we drop this check?
Log.w(TAG, "Cursor not set on calendar item");
return;
}
// Do nothing if the selection didn't change so that reminders will not get lost
int idColumn = c.getColumnIndexOrThrow(Calendars._ID);
long calendarId = c.getLong(idColumn);
int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
int color = c.getInt(colorColumn);
int displayColor = Utils.getDisplayColorFromColor(color);
// Prevents resetting of data (reminders, etc.) on orientation change.
if (calendarId == mModel.mCalendarId && mModel.isCalendarColorInitialized() &&
displayColor == mModel.getCalendarColor()) {
return;
}
setSpinnerBackgroundColor(displayColor);
mModel.mCalendarId = calendarId;
mModel.setCalendarColor(displayColor);
mModel.mCalendarAccountName = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_NAME);
mModel.mCalendarAccountType = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_TYPE);
mModel.setEventColor(mModel.getCalendarColor());
setColorPickerButtonStates(mModel.getCalendarEventColors());
// Update the max/allowed reminders with the new calendar properties.
int maxRemindersColumn = c.getColumnIndexOrThrow(Calendars.MAX_REMINDERS);
mModel.mCalendarMaxReminders = c.getInt(maxRemindersColumn);
int allowedRemindersColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_REMINDERS);
mModel.mCalendarAllowedReminders = c.getString(allowedRemindersColumn);
int allowedAttendeeTypesColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_ATTENDEE_TYPES);
mModel.mCalendarAllowedAttendeeTypes = c.getString(allowedAttendeeTypesColumn);
int allowedAvailabilityColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_AVAILABILITY);
mModel.mCalendarAllowedAvailability = c.getString(allowedAvailabilityColumn);
// Discard the current reminders and replace them with the model's default reminder set.
// We could attempt to save & restore the reminders that have been added, but that's
// probably more trouble than it's worth.
mModel.mReminders.clear();
mModel.mReminders.addAll(mModel.mDefaultReminders);
mModel.mHasAlarm = mModel.mReminders.size() != 0;
// Update the UI elements.
mReminderItems.clear();
LinearLayout reminderLayout =
(LinearLayout) mScrollView.findViewById(R.id.reminder_items_container);
reminderLayout.removeAllViews();
prepareReminders();
prepareAvailability();
}
/**
* Checks if the start and end times for this event should be displayed in
* the Calendar app's time zone as well and formats and displays them.
*/
private void updateHomeTime() {
String tz = Utils.getTimeZone(mActivity, null);
if (!mAllDayCheckBox.isChecked() && !TextUtils.equals(tz, mTimezone)
&& mModification != EditEventHelper.MODIFY_UNINITIALIZED) {
int flags = DateUtils.FORMAT_SHOW_TIME;
boolean is24Format = DateFormat.is24HourFormat(mActivity);
if (is24Format) {
flags |= DateUtils.FORMAT_24HOUR;
}
long millisStart = mStartTime.toMillis(false);
long millisEnd = mEndTime.toMillis(false);
boolean isDSTStart = mStartTime.isDst != 0;
boolean isDSTEnd = mEndTime.isDst != 0;
// First update the start date and times
String tzDisplay = TimeZone.getTimeZone(tz).getDisplayName(
isDSTStart, TimeZone.SHORT, Locale.getDefault());
StringBuilder time = new StringBuilder();
mSB.setLength(0);
time.append(DateUtils
.formatDateRange(mActivity, mF, millisStart, millisStart, flags, tz))
.append(" ").append(tzDisplay);
mStartTimeHome.setText(time.toString());
flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY;
mSB.setLength(0);
mStartDateHome
.setText(DateUtils.formatDateRange(
mActivity, mF, millisStart, millisStart, flags, tz).toString());
// Make any adjustments needed for the end times
if (isDSTEnd != isDSTStart) {
tzDisplay = TimeZone.getTimeZone(tz).getDisplayName(
isDSTEnd, TimeZone.SHORT, Locale.getDefault());
}
flags = DateUtils.FORMAT_SHOW_TIME;
if (is24Format) {
flags |= DateUtils.FORMAT_24HOUR;
}
// Then update the end times
time.setLength(0);
mSB.setLength(0);
time.append(DateUtils.formatDateRange(
mActivity, mF, millisEnd, millisEnd, flags, tz)).append(" ").append(tzDisplay);
mEndTimeHome.setText(time.toString());
flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY;
mSB.setLength(0);
mEndDateHome.setText(DateUtils.formatDateRange(
mActivity, mF, millisEnd, millisEnd, flags, tz).toString());
mStartHomeGroup.setVisibility(View.VISIBLE);
mEndHomeGroup.setVisibility(View.VISIBLE);
} else {
mStartHomeGroup.setVisibility(View.GONE);
mEndHomeGroup.setVisibility(View.GONE);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
}