blob: 3ef527e2c087d34fbdfe788d58388fe6b1637bd1 [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.DatePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.ProgressDialog;
import android.app.Service;
import android.app.TimePickerDialog;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.ContentResolver;
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.database.MatrixCursor;
import android.graphics.drawable.Drawable;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.provider.CalendarContract;
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.LayoutInflater;
import android.view.View;
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.CalendarView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
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 android.widget.TimePicker;
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.GeneralPreferences;
import com.android.calendar.R;
import com.android.calendar.RecipientAdapter;
import com.android.calendar.TimezoneAdapter;
import com.android.calendar.TimezoneAdapter.TimezoneRow;
import com.android.calendar.Utils;
import com.android.calendar.event.EditEventHelper.EditDoneRunnable;
import com.android.calendarcommon2.EventRecurrence;
import com.android.common.Rfc822InputFilter;
import com.android.common.Rfc822Validator;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
import java.util.TreeMap;
public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener,
DialogInterface.OnClickListener, OnItemSelectedListener {
private static final String TAG = "EditEvent";
private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com";
private static final String PERIOD_SPACE = ". ";
// Constants used for title autocompletion.
private static final String[] EVENT_PROJECTION = new String[] {
Events._ID,
Events.TITLE,
};
private static final int EVENT_INDEX_ID = 0;
private static final int EVENT_INDEX_TITLE = 1;
private static final String TITLE_WHERE = Events.TITLE + " LIKE ?";
private static final int MAX_TITLE_SUGGESTIONS = 4;
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 mTimezoneRow;
TextView mStartTimeHome;
TextView mStartDateHome;
TextView mEndTimeHome;
TextView mEndDateHome;
CheckBox mAllDayCheckBox;
Spinner mCalendarsSpinner;
Spinner mRepeatsSpinner;
Spinner mAvailabilitySpinner;
Spinner mAccessLevelSpinner;
RadioGroup mResponseRadioGroup;
AutoCompleteTextView mTitleTextView;
TextView mLocationTextView;
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];
private int[] mOriginalSpinnerPadding = new int[4];
private boolean mIsMultipane;
private ProgressDialog mLoadingCalendarsDialog;
private AlertDialog mNoCalendarsDialog;
private AlertDialog mTimezoneDialog;
private Activity mActivity;
private EditDoneRunnable mDone;
private View mView;
private CalendarEventModel mModel;
private Cursor mCalendarsCursor;
private AccountSpecifier mAddressAdapter;
private Rfc822Validator mEmailValidator;
private TimezoneAdapter mTimezoneAdapter;
private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer>(0);
/**
* 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 int mDefaultReminderMinutes;
private boolean mSaveAfterQueryComplete = false;
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 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(TimePicker 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 tp = new TimePickerDialog(mActivity, new TimeListener(v), mTime.hour,
mTime.minute, DateFormat.is24HourFormat(mActivity));
tp.setCanceledOnTouchOutside(true);
tp.show();
}
}
private class DateListener implements OnDateSetListener {
View mView;
public DateListener(View view) {
mView = view;
}
@Override
public void onDateSet(DatePicker 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));
}
private void populateTimezone(long eventStartTime) {
if (mTimezoneAdapter == null) {
mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone, eventStartTime);
} else {
mTimezoneAdapter.setTime(eventStartTime);
}
if (mTimezoneDialog != null) {
mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter);
}
mTimezoneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showTimezoneDialog();
}
});
setTimezone(mTimezoneAdapter.getRowById(mTimezone));
}
private void showTimezoneDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
final Context alertDialogContext = builder.getContext();
builder.setTitle(R.string.timezone_label);
builder.setSingleChoiceItems(
mTimezoneAdapter, mTimezoneAdapter.getRowById(mTimezone), this);
mTimezoneDialog = builder.create();
LayoutInflater layoutInflater = (LayoutInflater) alertDialogContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final TextView timezoneFooterView = (TextView) layoutInflater.inflate(
R.layout.timezone_footer, null);
timezoneFooterView.setText(mActivity.getString(R.string.edit_event_show_all) + " >");
timezoneFooterView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTimezoneDialog.getListView().removeFooterView(timezoneFooterView);
mTimezoneAdapter.showAllTimezones();
final int row = mTimezoneAdapter.getRowById(mTimezone);
// we need to post the selection changes to have them have
// any effect
mTimezoneDialog.getListView().post(new Runnable() {
@Override
public void run() {
mTimezoneDialog.getListView().setItemChecked(row, true);
mTimezoneDialog.getListView().setSelection(row);
}
});
}
});
mTimezoneDialog.getListView().addFooterView(timezoneFooterView);
mTimezoneDialog.setCanceledOnTouchOutside(true);
mTimezoneDialog.show();
}
private void populateRepeats() {
Time time = mStartTime;
Resources r = mActivity.getResources();
String[] days = new String[] {
DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM), };
String[] ordinals = r.getStringArray(R.array.ordinal_labels);
// Only display "Custom" in the spinner if the device does not support
// the recurrence functionality of the event. Only display every weekday
// if the event starts on a weekday.
boolean isCustomRecurrence = isCustomRecurrence();
boolean isWeekdayEvent = isWeekdayEvent();
ArrayList<String> repeatArray = new ArrayList<String>(0);
ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
repeatArray.add(r.getString(R.string.does_not_repeat));
recurrenceIndexes.add(EditEventHelper.DOES_NOT_REPEAT);
repeatArray.add(r.getString(R.string.daily));
recurrenceIndexes.add(EditEventHelper.REPEATS_DAILY);
if (isWeekdayEvent) {
repeatArray.add(r.getString(R.string.every_weekday));
recurrenceIndexes.add(EditEventHelper.REPEATS_EVERY_WEEKDAY);
}
String format = r.getString(R.string.weekly);
repeatArray.add(String.format(format, time.format("%A")));
recurrenceIndexes.add(EditEventHelper.REPEATS_WEEKLY_ON_DAY);
// Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance
// of the given day.
int dayNumber = (time.monthDay - 1) / 7;
format = r.getString(R.string.monthly_on_day_count);
repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT);
format = r.getString(R.string.monthly_on_day);
repeatArray.add(String.format(format, time.monthDay));
recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY);
long when = time.toMillis(false);
format = r.getString(R.string.yearly);
int flags = 0;
if (DateFormat.is24HourFormat(mActivity)) {
flags |= DateUtils.FORMAT_24HOUR;
}
repeatArray.add(String.format(format, DateUtils.formatDateTime(mActivity, when, flags)));
recurrenceIndexes.add(EditEventHelper.REPEATS_YEARLY);
if (isCustomRecurrence) {
repeatArray.add(r.getString(R.string.custom));
recurrenceIndexes.add(EditEventHelper.REPEATS_CUSTOM);
}
mRecurrenceIndexes = recurrenceIndexes;
int position = recurrenceIndexes.indexOf(EditEventHelper.DOES_NOT_REPEAT);
if (!TextUtils.isEmpty(mModel.mRrule)) {
if (isCustomRecurrence) {
position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_CUSTOM);
} else {
switch (mEventRecurrence.freq) {
case EventRecurrence.DAILY:
position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_DAILY);
break;
case EventRecurrence.WEEKLY:
if (mEventRecurrence.repeatsOnEveryWeekDay()) {
position = recurrenceIndexes.indexOf(
EditEventHelper.REPEATS_EVERY_WEEKDAY);
} else {
position = recurrenceIndexes.indexOf(
EditEventHelper.REPEATS_WEEKLY_ON_DAY);
}
break;
case EventRecurrence.MONTHLY:
if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
position = recurrenceIndexes.indexOf(
EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT);
} else {
position = recurrenceIndexes.indexOf(
EditEventHelper.REPEATS_MONTHLY_ON_DAY);
}
break;
case EventRecurrence.YEARLY:
position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_YEARLY);
break;
}
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
android.R.layout.simple_spinner_item, repeatArray);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mRepeatsSpinner.setAdapter(adapter);
mRepeatsSpinner.setSelection(position);
// Don't allow the user to make exceptions recurring events.
if (mModel.mOriginalSyncId != null) {
mRepeatsSpinner.setEnabled(false);
}
}
private boolean isCustomRecurrence() {
if (mEventRecurrence.until != null
|| (mEventRecurrence.interval != 0 && mEventRecurrence.interval != 1)
|| mEventRecurrence.count != 0) {
return true;
}
if (mEventRecurrence.freq == 0) {
return false;
}
switch (mEventRecurrence.freq) {
case EventRecurrence.DAILY:
return false;
case EventRecurrence.WEEKLY:
if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
return false;
} else if (mEventRecurrence.bydayCount == 1) {
return false;
}
break;
case EventRecurrence.MONTHLY:
if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
/* this is a "3rd Tuesday of every month" sort of rule */
return false;
} else if (mEventRecurrence.bydayCount == 0
&& mEventRecurrence.bymonthdayCount == 1
&& mEventRecurrence.bymonthday[0] > 0) {
/* this is a "22nd day of every month" sort of rule */
return false;
}
break;
case EventRecurrence.YEARLY:
return false;
}
return true;
}
private boolean isWeekdayEvent() {
if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
return true;
}
return false;
}
private class DateClickListener implements View.OnClickListener {
private Time mTime;
public DateClickListener(Time time) {
mTime = time;
}
public void onClick(View v) {
DatePickerDialog dpd = new DatePickerDialog(
mActivity, new DateListener(v), mTime.year, mTime.month, mTime.monthDay);
CalendarView cv = dpd.getDatePicker().getCalendarView();
cv.setShowWeekNumber(Utils.getShowWeekNumber(mActivity));
int startOfWeek = Utils.getFirstDayOfWeek(mActivity);
// Utils returns Time days while CalendarView wants Calendar days
if (startOfWeek == Time.SATURDAY) {
startOfWeek = Calendar.SATURDAY;
} else if (startOfWeek == Time.SUNDAY) {
startOfWeek = Calendar.SUNDAY;
} else {
startOfWeek = Calendar.MONDAY;
}
cv.setFirstDayOfWeek(startOfWeek);
dpd.setCanceledOnTouchOutside(true);
dpd.show();
}
}
static private class CalendarsAdapter extends ResourceCursorAdapter {
public CalendarsAdapter(Context context, Cursor c) {
super(context, R.layout.calendars_item, 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);
}
}
}
}
/**
* Adapter for title auto completion.
*/
private static class TitleAdapter extends ResourceCursorAdapter {
private final ContentResolver mContentResolver;
public TitleAdapter(Context context) {
super(context, android.R.layout.simple_dropdown_item_1line, null, 0);
mContentResolver = context.getContentResolver();
}
@Override
public int getCount() {
return Math.min(MAX_TITLE_SUGGESTIONS, super.getCount());
}
private static String getTitleAtCursor(Cursor cursor) {
return cursor.getString(EVENT_INDEX_TITLE);
}
@Override
public final String convertToString(Cursor cursor) {
return getTitleAtCursor(cursor);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView textView = (TextView) view;
textView.setText(getTitleAtCursor(cursor));
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
String filter = constraint == null ? "" : constraint.toString() + "%";
if (filter.isEmpty()) {
return null;
}
long startTime = System.currentTimeMillis();
// Query all titles prefixed with the constraint. There is no way to insert
// 'DISTINCT' or 'GROUP BY' to get rid of dupes, so use post-processing to
// remove dupes. We will order query results by descending event ID to show
// results that were most recently inputted.
Cursor tempCursor = mContentResolver.query(Events.CONTENT_URI, EVENT_PROJECTION,
TITLE_WHERE, new String[] { filter }, Events._ID + " DESC");
if (tempCursor != null) {
try {
// Post process query results.
Cursor c = uniqueTitlesCursor(tempCursor);
// Log the processing duration.
long duration = System.currentTimeMillis() - startTime;
StringBuilder msg = new StringBuilder();
msg.append("Autocomplete of ");
msg.append(constraint);
msg.append(": title query match took ");
msg.append(duration);
msg.append("ms.");
Log.d(TAG, msg.toString());
return c;
} finally {
tempCursor.close();
}
} else {
return null;
}
}
/**
* Post-process the query results to return the first MAX_TITLE_SUGGESTIONS
* unique titles in alphabetical order.
*/
private Cursor uniqueTitlesCursor(Cursor cursor) {
TreeMap<String, String[]> titleToQueryResults =
new TreeMap<String, String[]>(String.CASE_INSENSITIVE_ORDER);
int numColumns = cursor.getColumnCount();
cursor.moveToPosition(-1);
// Remove dupes.
while ((titleToQueryResults.size() < MAX_TITLE_SUGGESTIONS) && cursor.moveToNext()) {
String title = getTitleAtCursor(cursor).trim();
String data[] = new String[numColumns];
if (!titleToQueryResults.containsKey(title)) {
for (int i = 0; i < numColumns; i++) {
data[i] = cursor.getString(i);
}
titleToQueryResults.put(title, data);
}
}
// Copy the sorted results to a new cursor.
MatrixCursor newCursor = new MatrixCursor(EVENT_PROJECTION);
for (String[] result : titleToQueryResults.values()) {
newCursor.addRow(result);
}
newCursor.moveToFirst();
return newCursor;
}
}
/**
* 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) {
// 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);
}
// 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);
}
} else if (dialog == mTimezoneDialog) {
if (which >= 0 && which < mTimezoneAdapter.getCount()) {
setTimezone(which);
updateHomeTime();
dialog.dismiss();
}
}
}
// 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());
int selection;
// If we're making an exception we don't want it to be a repeating
// event.
if (mModification == EditEventHelper.MODIFY_SELECTED) {
selection = EditEventHelper.DOES_NOT_REPEAT;
} else {
int position = mRepeatsSpinner.getSelectedItemPosition();
selection = mRecurrenceIndexes.get(position);
}
EditEventHelper.updateRecurrenceRule(
selection, mModel, Utils.getFirstDayOfWeek(mActivity) + 1);
// Save the timezone so we can display it as a standard option next time
if (!mModel.mAllDay) {
mTimezoneAdapter.saveRecentTimezone(mTimezone);
}
return true;
}
public EditEventView(Activity activity, View view, EditDoneRunnable done) {
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 = (AutoCompleteTextView) view.findViewById(R.id.title);
mLocationTextView = (TextView) 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);
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);
mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats);
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);
mTitleTextView.setTag(mTitleTextView.getBackground());
mTitleTextView.setAdapter(new TitleAdapter(activity));
mTitleTextView.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.).
mTitleTextView.dismissDropDown();
}
return false;
}
});
mLocationTextView.setTag(mLocationTextView.getBackground());
mDescriptionTextView.setTag(mDescriptionTextView.getBackground());
mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground());
mAttendeesList.setTag(mAttendeesList.getBackground());
mOriginalPadding[0] = mLocationTextView.getPaddingLeft();
mOriginalPadding[1] = mLocationTextView.getPaddingTop();
mOriginalPadding[2] = mLocationTextView.getPaddingRight();
mOriginalPadding[3] = mLocationTextView.getPaddingBottom();
mOriginalSpinnerPadding[0] = mRepeatsSpinner.getPaddingLeft();
mOriginalSpinnerPadding[1] = mRepeatsSpinner.getPaddingTop();
mOriginalSpinnerPadding[2] = mRepeatsSpinner.getPaddingRight();
mOriginalSpinnerPadding[3] = mRepeatsSpinner.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);
}
/**
* 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);
if (mModel.mCalendarAllowedAvailability != null) {
EventViewUtils.reduceMethodList(mAvailabilityValues, mAvailabilityLabels,
mModel.mCalendarAllowedAvailability);
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
android.R.layout.simple_spinner_item, mAvailabilityLabels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mAvailabilitySpinner.setAdapter(adapter);
}
/**
* 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);
}
String rrule = model.mRrule;
if (!TextUtils.isEmpty(rrule)) {
mEventRecurrence.parse(rrule);
}
// 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);
}
int displayColor = Utils.getDisplayColorFromColor(model.mCalendarColor);
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);
}
if (mIsMultipane) {
mView.findViewById(R.id.calendar_textview).setBackgroundColor(displayColor);
} else {
mView.findViewById(R.id.calendar_group).setBackgroundColor(displayColor);
}
} else {
View calendarGroup = mView.findViewById(R.id.calendar_group);
calendarGroup.setVisibility(View.GONE);
}
populateWhen();
populateRepeats();
updateAttendees(model.mAttendeesList);
updateView();
mScrollView.setVisibility(View.VISIBLE);
mLoadingMessage.setVisibility(View.GONE);
sendAccessibilityEvent();
}
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) {
// 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 defaultCalendarPosition = findDefaultCalendarPosition(cursor);
// populate the calendars spinner
CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor);
mCalendarsSpinner.setAdapter(adapter);
mCalendarsSpinner.setSelection(defaultCalendarPosition);
mCalendarsSpinner.setOnItemSelectedListener(this);
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);
mRepeatsSpinner.setEnabled(false);
mRepeatsSpinner.setBackgroundDrawable(null);
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);
}
mRepeatsSpinner.setBackgroundDrawable((Drawable) mRepeatsSpinner.getTag());
mRepeatsSpinner.setPadding(mOriginalSpinnerPadding[0], mOriginalSpinnerPadding[1],
mOriginalSpinnerPadding[2], mOriginalSpinnerPadding[3]);
if (mModel.mOriginalSyncId == null) {
mRepeatsSpinner.setEnabled(true);
} else {
mRepeatsSpinner.setEnabled(false);
}
mRemindersGroup.setVisibility(View.VISIBLE);
mLocationGroup.setVisibility(View.VISIBLE);
mDescriptionGroup.setVisibility(View.VISIBLE);
}
setAllDayViewsVisibility(mAllDayCheckBox.isChecked());
}
public void setModification(int modifyWhich) {
mModification = modifyWhich;
updateView();
updateHomeTime();
}
// 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 calendarsOwnerColumn = 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(calendarsOwnerColumn);
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()) {
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;
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);
}
private void setTimezone(int i) {
if (i < 0 || i >= mTimezoneAdapter.getCount()) {
return; // do nothing
}
TimezoneRow timezone = mTimezoneAdapter.getItem(i);
mTimezoneTextView.setText(timezone.toString());
mTimezoneButton.setText(timezone.toString());
mTimezone = timezone.mId;
mStartTime.timezone = mTimezone;
mStartTime.normalize(true);
mEndTime.timezone = mTimezone;
mEndTime.normalize(true);
mTimezoneAdapter.setCurrentTimezone(mTimezone);
}
/**
* @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);
}
mAllDay = isChecked;
updateHomeTime();
}
@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;
}
int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
int color = c.getInt(colorColumn);
int displayColor = Utils.getDisplayColorFromColor(color);
if (mIsMultipane) {
mCalendarSelectorWrapper.setBackgroundColor(displayColor);
} else {
mCalendarSelectorGroup.setBackgroundColor(displayColor);
}
// 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);
if (calendarId == mModel.mCalendarId) {
return;
}
mModel.mCalendarId = calendarId;
mModel.mCalendarColor = color;
// 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) {
}
}