summaryrefslogtreecommitdiffstats
path: root/src/com/android/calendar/recurrencepicker
diff options
context:
space:
mode:
authorMichael Chan <mchan@android.com>2013-03-14 02:53:30 -0700
committerMichael Chan <mchan@android.com>2013-03-19 22:27:40 -0700
commitb21c638ca11d9be3a3d9e7d28223bb4a3dab5f15 (patch)
tree47ebbc711490aad11ef4652cc8534866cb07c8ee /src/com/android/calendar/recurrencepicker
parentd5bcbad7d3e82199bcb00d94c1d7f757f9b7e7fc (diff)
downloadandroid_packages_apps_Calendar-b21c638ca11d9be3a3d9e7d28223bb4a3dab5f15.tar.gz
android_packages_apps_Calendar-b21c638ca11d9be3a3d9e7d28223bb4a3dab5f15.tar.bz2
android_packages_apps_Calendar-b21c638ca11d9be3a3d9e7d28223bb4a3dab5f15.zip
First draft of Recurrence picker
Change-Id: I582c44bc4da6afab89fbbf88fda8262640b85e3b
Diffstat (limited to 'src/com/android/calendar/recurrencepicker')
-rw-r--r--src/com/android/calendar/recurrencepicker/LinearLayoutWithMaxWidth.java43
-rw-r--r--src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java1164
-rw-r--r--src/com/android/calendar/recurrencepicker/WeekButton.java58
3 files changed, 1265 insertions, 0 deletions
diff --git a/src/com/android/calendar/recurrencepicker/LinearLayoutWithMaxWidth.java b/src/com/android/calendar/recurrencepicker/LinearLayoutWithMaxWidth.java
new file mode 100644
index 00000000..455a039c
--- /dev/null
+++ b/src/com/android/calendar/recurrencepicker/LinearLayoutWithMaxWidth.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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.recurrencepicker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+public class LinearLayoutWithMaxWidth extends LinearLayout {
+
+ public LinearLayoutWithMaxWidth(Context context) {
+ super(context);
+ }
+
+ public LinearLayoutWithMaxWidth(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LinearLayoutWithMaxWidth(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ WeekButton.setSuggestedWidth((View.MeasureSpec.getSize(widthMeasureSpec)) / 7);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java b/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java
new file mode 100644
index 00000000..ac5f3191
--- /dev/null
+++ b/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java
@@ -0,0 +1,1164 @@
+/*
+ * Copyright (C) 2013 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.recurrencepicker;
+
+import android.app.DatePickerDialog;
+import android.app.DatePickerDialog.OnDateSetListener;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.util.TimeFormatException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+
+import com.android.calendar.R;
+import com.android.calendar.Utils;
+import com.android.calendarcommon2.EventRecurrence;
+
+import java.text.DateFormatSymbols;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+
+public class RecurrencePickerDialog extends DialogFragment implements OnItemSelectedListener,
+ OnCheckedChangeListener, OnClickListener,
+ android.widget.RadioGroup.OnCheckedChangeListener, DatePickerDialog.OnDateSetListener {
+
+ private static final String TAG = "RecurrencePickerDialog";
+
+ // Update android:maxLength in EditText as needed
+ private static final int INTERVAL_MAX = 99;
+ private static final int INTERVAL_DEFAULT = 1;
+ // Update android:maxLength in EditText as needed
+ private static final int COUNT_MAX = 730;
+ private static final int COUNT_DEFAULT = 5;
+
+ private class Model implements Parcelable {
+
+ // Not repeating
+ static final int FREQ_NONE = -1;
+
+ // Should match EventRecurrence.DAILY, etc
+ static final int FREQ_DAILY = 0;
+ static final int FREQ_WEEKLY = 1;
+ static final int FREQ_MONTHLY = 2;
+ static final int FREQ_YEARLY = 3;
+
+ static final int END_NEVER = 0;
+ static final int END_BY_DATE = 1;
+ static final int END_BY_COUNT = 2;
+
+ static final int MONTHLY_BY_DATE = 0;
+ static final int MONTHLY_BY_NTH_DAY_OF_WEEK = 1;
+
+ /**
+ * FREQ: Repeat pattern
+ *
+ * @see FREQ_DAILY
+ * @see FREQ_WEEKLY
+ * @see FREQ_MONTHLY
+ * @see FREQ_YEARLY
+ */
+ int freq = FREQ_NONE;
+
+ /**
+ * INTERVAL: Every n days/weeks/months/years. n >= 1
+ */
+ int interval = INTERVAL_DEFAULT;
+
+ /**
+ * UNTIL and COUNT: How does the the event end?
+ *
+ * @see END_NEVER
+ * @see END_BY_DATE
+ * @see END_BY_COUNT
+ * @see untilDate
+ * @see untilCount
+ */
+ int end;
+
+ /**
+ * UNTIL: Date of the last recurrence. Used when until == END_BY_DATE
+ */
+ Time endDate;
+
+ /**
+ * COUNT: Times to repeat. Use when until == END_BY_COUNT
+ */
+ int endCount = COUNT_DEFAULT;
+
+ /**
+ * BYDAY: Days of the week to be repeated. Sun = 0, Mon = 1, etc
+ */
+ boolean[] weeklyByDayOfWeek = new boolean[7];
+
+ /**
+ * BYDAY AND BYMONTHDAY: How to repeat monthly events? Same date of the
+ * month or Same nth day of week.
+ *
+ * @see MONTHLY_BY_DATE
+ * @see MONTHLY_BY_NTH_DAY_OF_WEEK
+ */
+ int monthlyRepeat;
+
+ /**
+ * Day of the month to repeat. Used when monthlyRepeat ==
+ * MONTHLY_BY_DATE
+ */
+ int monthlyByMonthDay;
+
+ /**
+ * Day of the week to repeat. Used when monthlyRepeat ==
+ * MONTHLY_BY_NTH_DAY_OF_WEEK
+ */
+ int monthlyByDayOfWeek;
+
+ /**
+ * Nth day of the week to repeat. Used when monthlyRepeat ==
+ * MONTHLY_BY_NTH_DAY_OF_WEEK 0=undefined, 1=1st, 2=2nd, etc
+ */
+ int monthlyByNthDayOfWeek;
+
+ /*
+ * (generated method)
+ */
+ @Override
+ public String toString() {
+ return "Model [freq=" + freq + ", interval=" + interval + ", end=" + end + ", endDate="
+ + endDate + ", endCount=" + endCount + ", weeklyByDayOfWeek="
+ + Arrays.toString(weeklyByDayOfWeek) + ", monthlyRepeat=" + monthlyRepeat
+ + ", monthlyByMonthDay=" + monthlyByMonthDay + ", monthlyByDayOfWeek="
+ + monthlyByDayOfWeek + ", monthlyByNthDayOfWeek=" + monthlyByNthDayOfWeek + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public Model() {
+ }
+
+ // protected Model(Parcel in) {
+ // freq = in.readInt();
+ // interval = in.readInt();
+ // end = in.readInt();
+ // endDate = new Time(); // TODO timezone?
+ // endDate.hour = endDate.minute = endDate.second = 0;
+ // endDate.year = in.readInt();
+ // endDate.month = in.readInt();
+ // endDate.monthDay = in.readInt();
+ // endCount = in.readInt();
+ // in.readBooleanArray(weeklyByDayOfWeek);
+ // monthlyRepeat = in.readInt();
+ // monthlyByMonthDay = in.readInt();
+ // monthlyByDayOfWeek = in.readInt();
+ // monthlyByNthDayOfWeek = in.readInt();
+ // }
+ //
+ // public static final Parcelable.Creator<Model> CREATOR = new
+ // Parcelable.Creator<Model>() {
+ // @Override
+ // public Model createFromParcel(Parcel in) {
+ // return new Model(in);
+ // }
+ //
+ // @Override
+ // public Model[] newArray(int size) {
+ // return new Model[size];
+ // }
+ // };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(freq);
+ dest.writeInt(interval);
+ dest.writeInt(end);
+ dest.writeInt(endDate.year);
+ dest.writeInt(endDate.month);
+ dest.writeInt(endDate.monthDay);
+ dest.writeInt(endCount);
+ dest.writeBooleanArray(weeklyByDayOfWeek);
+ dest.writeInt(monthlyRepeat);
+ dest.writeInt(monthlyByMonthDay);
+ dest.writeInt(monthlyByDayOfWeek);
+ dest.writeInt(monthlyByNthDayOfWeek);
+ }
+ }
+
+ class minMaxTextWatcher implements TextWatcher {
+ private int mMin;
+ private int mMax;
+ private int mDefault;
+
+ public minMaxTextWatcher(int min, int defaultInt, int max) {
+ mMin = min;
+ mMax = max;
+ mDefault = defaultInt;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ boolean updated = false;
+ int value;
+ try {
+ value = Integer.parseInt(s.toString());
+ } catch (NumberFormatException e) {
+ value = mDefault;
+ }
+
+ if (value < mMin) {
+ value = mMin;
+ updated = true;
+ } else if (value > mMax) {
+ updated = true;
+ value = mMax;
+ }
+
+ // Update UI
+ if (updated) {
+ s.clear();
+ s.append(Integer.toString(value));
+ }
+
+ onChange(value);
+ }
+
+ /** Override to be called after each key stroke */
+ void onChange(int value) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ }
+
+ private Resources mResources;
+ private EventRecurrence mRecurrence = new EventRecurrence();
+ private Time mTime = new Time(); // TODO timezone?
+ private Model mModel = new Model();
+ private Toast mToast;
+
+ private final int[] TIME_DAY_TO_CALENDAR_DAY = new int[] {
+ Calendar.SUNDAY,
+ Calendar.MONDAY,
+ Calendar.TUESDAY,
+ Calendar.WEDNESDAY,
+ Calendar.THURSDAY,
+ Calendar.FRIDAY,
+ Calendar.SATURDAY,
+ };
+
+ // Call mStringBuilder.setLength(0) before formatting any string or else the
+ // formatted text will accumulate.
+ // private final StringBuilder mStringBuilder = new StringBuilder();
+ // private Formatter mFormatter = new Formatter(mStringBuilder);
+
+ private View mView;
+
+ private Spinner mFreqSpinner;
+ private static final int[] mFreqModelToEventRecurrence = {
+ EventRecurrence.DAILY,
+ EventRecurrence.WEEKLY,
+ EventRecurrence.MONTHLY,
+ EventRecurrence.YEARLY
+ };
+
+ public static final String BUNDLE_START_TIME_MILLIS = "bundle_event_start_time";
+ public static final String BUNDLE_TIME_ZONE = "bundle_event_time_zone";
+ public static final String BUNDLE_RRULE = "bundle_event_rrule";
+
+ private static final String BUNDLE_MODEL = "bundle_model";
+ private static final String BUNDLE_END_YEAR = "bundle_end_year";
+ private static final String BUNDLE_END_MONTH = "bundle_end_month";
+ private static final String BUNDLE_END_DAY = "bundle_end_day";
+
+ private static final String FRAG_TAG_DATE_PICKER = "tag_date_picker_frag";
+
+ private LinearLayout mIntervalGroup;
+ private EditText mInterval;
+ private TextView mIntervalPreText;
+ private TextView mIntervalPostText;
+
+ private LinearLayout mEndGroup;
+ private Spinner mEndSpinner;
+ private Button mEndDateTextView;
+ private EditText mEndCount;
+
+ private ArrayList<CharSequence> mEndSpinnerArray = new ArrayList<CharSequence>(3);
+ private ArrayAdapter<CharSequence> mEndSpinnerAdapter;
+ private String mEndNeverStr;
+
+ /** Hold toggle buttons in the order per user's first day of week preference */
+ private LinearLayout mWeekGroup;
+ // Sun = 0
+ private ToggleButton[] mWeekByDayButtons = new ToggleButton[7];
+ private String[] mDayOfWeekString;
+ private String[] mOrdinalArray;
+
+ private LinearLayout mMonthGroup;
+ private RadioGroup mMonthRepeatByRadioGroup;
+ private RadioButton mMonthRepeatByNthDayOfWeek;
+ private String mMonthRepeatByDayOfWeekStr;
+
+ private Button mDone;
+
+ private OnRecurrenceSetListener mRecurrenceSetListener;
+
+ public RecurrencePickerDialog() {
+ }
+
+ static public boolean canHandleRecurrenceRule(EventRecurrence er) {
+ switch (er.freq) {
+ case EventRecurrence.DAILY:
+ case EventRecurrence.MONTHLY:
+ case EventRecurrence.YEARLY:
+ case EventRecurrence.WEEKLY:
+ break;
+ default:
+ return false;
+ }
+
+ if (er.count > 0 && !TextUtils.isEmpty(er.until)) {
+ return false;
+ }
+
+ // Weekly: For "repeat by day of week", the day of week to repeat is in
+ // er.byday[]
+
+ /*
+ * Monthly: For "repeat by nth day of week" the day of week to repeat is
+ * in er.byday[] and the "nth" is stored in er.bydayNum[]. Currently we
+ * can handle only one and only in monthly
+ */
+ int numOfByDayNum = 0;
+ for (int i = 0; i < er.bydayCount; i++) {
+ if (er.bydayNum[i] > 0) {
+ ++numOfByDayNum;
+ }
+ }
+
+ if (numOfByDayNum > 1) {
+ return false;
+ }
+
+ if (numOfByDayNum > 0 && er.freq != EventRecurrence.MONTHLY) {
+ return false;
+ }
+
+ // The UI only handle repeat by one day of month i.e. not 9th and 10th
+ // of every month
+ if (er.bymonthdayCount > 1) {
+ return false;
+ }
+
+ if (er.freq == EventRecurrence.MONTHLY) {
+ if (er.bydayCount > 1) {
+ return false;
+ }
+ if (er.bydayCount > 0 && er.bymonthdayCount > 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // TODO compare
+// 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;
+// }
+
+
+ // TODO don't lose data when getting data that our UI can't handle
+ static private void copyEventRecurrenceToModel(final EventRecurrence er, Model model) {
+ // Freq:
+ switch (er.freq) {
+ case EventRecurrence.DAILY:
+ model.freq = Model.FREQ_DAILY;
+ break;
+ case EventRecurrence.MONTHLY:
+ model.freq = Model.FREQ_MONTHLY;
+ break;
+ case EventRecurrence.YEARLY:
+ model.freq = Model.FREQ_YEARLY;
+ break;
+ case EventRecurrence.WEEKLY:
+ model.freq = Model.FREQ_WEEKLY;
+ break;
+ default:
+ throw new IllegalStateException("freq=" + er.freq);
+ }
+
+ // Interval:
+ if (er.interval > 0) {
+ model.interval = er.interval;
+ }
+
+ // End:
+ // End by count:
+ model.endCount = er.count;
+ if (model.endCount > 0) {
+ model.end = Model.END_BY_COUNT;
+ }
+
+ // End by date:
+ if (!TextUtils.isEmpty(er.until)) {
+ if (model.endDate == null) {
+ model.endDate = new Time();
+ }
+
+ try {
+ model.endDate.parse(er.until);
+ } catch (TimeFormatException e) {
+ model.endDate = null;
+ }
+
+ // LIMITATION: The UI can only handle END_BY_DATE or END_BY_COUNT
+ if (model.end == Model.END_BY_COUNT && model.endDate != null) {
+ throw new IllegalStateException("freq=" + er.freq);
+ }
+
+ model.end = Model.END_BY_DATE;
+ }
+
+ // Weekly: repeat by day of week or Monthly: repeat by nth day of week
+ // in the month
+ Arrays.fill(model.weeklyByDayOfWeek, false);
+ if (er.bydayCount > 0) {
+ int count = 0;
+ for (int i = 0; i < er.bydayCount; i++) {
+ int dayOfWeek = EventRecurrence.day2TimeDay(er.byday[i]);
+ model.weeklyByDayOfWeek[dayOfWeek] = true;
+
+ if (model.freq == Model.FREQ_MONTHLY && er.bydayNum[i] > 0) {
+ // LIMITATION: Can handle only (one) weekDayNum and only
+ // when
+ // monthly
+ model.monthlyByDayOfWeek = dayOfWeek;
+ model.monthlyByNthDayOfWeek = er.bydayNum[i];
+ model.monthlyRepeat = Model.MONTHLY_BY_NTH_DAY_OF_WEEK;
+ count++;
+ }
+ }
+
+ if (model.freq == Model.FREQ_MONTHLY) {
+ if (er.bydayCount != 1) {
+ // Can't handle 1st Monday and 2nd Wed
+ throw new IllegalStateException("Can handle only 1 byDayOfWeek in monthly");
+ }
+ if (count != 1) {
+ throw new IllegalStateException(
+ "Didn't specify which nth day of week to repeat for a monthly");
+ }
+ }
+ }
+
+ // Monthly by day of month
+ if (model.freq == Model.FREQ_MONTHLY) {
+ if (er.bymonthdayCount == 1) {
+ if (model.monthlyRepeat == Model.MONTHLY_BY_NTH_DAY_OF_WEEK) {
+ throw new IllegalStateException(
+ "Can handle only by monthday or by nth day of week, not both");
+ }
+ model.monthlyByMonthDay = er.bymonthday[0];
+ model.monthlyRepeat = Model.MONTHLY_BY_DATE;
+ } else if (er.bymonthCount > 1) {
+ // LIMITATION: Can handle only one month day
+ throw new IllegalStateException("Can handle only one bymonthday");
+ }
+ }
+ }
+
+ static private void copyModelToEventRecurrence(final Model model, EventRecurrence er) {
+ if (model.freq == Model.FREQ_NONE) {
+ throw new IllegalStateException("There's no recurrence");
+ }
+
+ // Freq
+ er.freq = mFreqModelToEventRecurrence[model.freq];
+
+ // Interval
+ if (model.interval <= 1) {
+ er.interval = 0;
+ } else {
+ er.interval = model.interval;
+ }
+
+ // End
+ switch (model.end) {
+ case Model.END_BY_DATE:
+ if (model.endDate != null) {
+ model.endDate.switchTimezone(Time.TIMEZONE_UTC);
+ model.endDate.normalize(false);
+ er.until = model.endDate.format2445();
+ er.count = 0;
+ } else {
+ throw new IllegalStateException("end = END_BY_DATE but endDate is null");
+ }
+ break;
+ case Model.END_BY_COUNT:
+ er.count = model.endCount;
+ er.until = null;
+ if (er.count <= 0) {
+ throw new IllegalStateException("count is " + er.count);
+ }
+ break;
+ default:
+ er.count = 0;
+ er.until = null;
+ break;
+ }
+
+ // Weekly && monthly repeat patterns
+ er.bydayCount = 0;
+ er.bymonthdayCount = 0;
+
+ switch (model.freq) {
+ case Model.FREQ_MONTHLY:
+ if (model.monthlyRepeat == Model.MONTHLY_BY_DATE) {
+ if (model.monthlyByMonthDay > 0) {
+ if (er.bymonthday == null || er.bymonthdayCount < 1) {
+ er.bymonthday = new int[1];
+ }
+ er.bymonthday[0] = model.monthlyByMonthDay;
+ er.bymonthdayCount = 1;
+ }
+ } else if (model.monthlyRepeat == Model.MONTHLY_BY_NTH_DAY_OF_WEEK) {
+ if (model.monthlyByNthDayOfWeek <= 0) {
+ throw new IllegalStateException("month repeat by nth week but n is "
+ + model.monthlyByNthDayOfWeek);
+ }
+ int count = 1;
+ if (er.bydayCount < count || er.byday == null || er.bydayNum == null) {
+ er.byday = new int[count];
+ er.bydayNum = new int[count];
+ }
+ er.bydayCount = count;
+ er.byday[0] = EventRecurrence.timeDay2Day(model.monthlyByDayOfWeek);
+ er.bydayNum[0] = model.monthlyByNthDayOfWeek;
+ }
+ break;
+ case Model.FREQ_WEEKLY:
+ int count = 0;
+ for (int i = 0; i < 7; i++) {
+ if (model.weeklyByDayOfWeek[i]) {
+ count++;
+ }
+ }
+
+ if (er.bydayCount < count || er.byday == null || er.bydayNum == null) {
+ er.byday = new int[count];
+ er.bydayNum = new int[count];
+ }
+ er.bydayCount = count;
+
+ for (int i = 6; i >= 0; i--) {
+ if (model.weeklyByDayOfWeek[i]) {
+ er.bydayNum[--count] = 0;
+ er.byday[count] = EventRecurrence.timeDay2Day(i);
+ }
+ }
+ break;
+ }
+
+ if (!canHandleRecurrenceRule(er)) {
+ throw new IllegalStateException("UI generated recurrence that it can't handle. ER:"
+ + er.toString() + " Model: " + model.toString());
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRecurrence.wkst = EventRecurrence.timeDay2Day(Utils.getFirstDayOfWeek(getActivity()));
+
+ if (savedInstanceState != null) {
+ Model m = (Model) savedInstanceState.get(BUNDLE_MODEL);
+ if (m != null) {
+ mModel = m;
+ }
+ } else {
+ Bundle b = getArguments();
+ if (b != null) {
+ mTime.set(b.getLong(BUNDLE_START_TIME_MILLIS));
+
+ String tz = b.getString(BUNDLE_TIME_ZONE);
+ if (!TextUtils.isEmpty(tz)) {
+ mTime.timezone = tz;
+ }
+ mTime.normalize(false);
+
+ // Time days of week: Sun=0, Mon=1, etc
+ mModel.weeklyByDayOfWeek[mTime.weekDay] = true;
+
+ String rrule = b.getString(BUNDLE_RRULE);
+ if (!TextUtils.isEmpty(rrule)) {
+ mRecurrence.parse(rrule);
+ copyEventRecurrenceToModel(mRecurrence, mModel);
+ }
+ } else {
+ mTime.setToNow();
+ }
+ }
+
+ mResources = getResources();
+ mView = inflater.inflate(R.layout.recurrencepicker, container);
+
+ mFreqSpinner = (Spinner) mView.findViewById(R.id.freqSpinner);
+ mFreqSpinner.setOnItemSelectedListener(this);
+ ArrayAdapter<CharSequence> freqAdapter = ArrayAdapter.createFromResource(getActivity(),
+ R.array.recurrence_freq, R.layout.recurrencepicker_freq_item);
+ freqAdapter.setDropDownViewResource(R.layout.recurrencepicker_freq_item);
+ mFreqSpinner.setAdapter(freqAdapter);
+
+ mIntervalGroup = (LinearLayout) mView.findViewById(R.id.intervalGroup);
+ mInterval = (EditText) mView.findViewById(R.id.interval);
+ mInterval.addTextChangedListener(new minMaxTextWatcher(1, INTERVAL_DEFAULT, INTERVAL_MAX) {
+ @Override
+ void onChange(int v) {
+ mModel.interval = v;
+ }
+ });
+ mIntervalPreText = (TextView) mView.findViewById(R.id.intervalPreText);
+ mIntervalPostText = (TextView) mView.findViewById(R.id.intervalPostText);
+
+ mEndGroup = (LinearLayout) mView.findViewById(R.id.endGroup);
+ mEndNeverStr = mResources.getString(R.string.recurrence_end_continously);
+ // mEndByDateFormatStr =
+ // mResources.getString(R.string.recurrence_end_date);
+ // mEndByCountFormatStr =
+ // mResources.getString(R.string.recurrence_end_count);
+ mEndSpinnerArray.add(mEndNeverStr);
+ mEndSpinnerArray.add(mResources.getString(R.string.recurrence_end_date_label));
+ mEndSpinnerArray.add(mResources.getString(R.string.recurrence_end_count_label));
+ mEndSpinner = (Spinner) mView.findViewById(R.id.endSpinner);
+ mEndSpinner.setOnItemSelectedListener(this);
+ mEndSpinnerAdapter = new ArrayAdapter<CharSequence>(getActivity(),
+ R.layout.recurrencepicker_freq_item, mEndSpinnerArray);
+ mEndSpinnerAdapter.setDropDownViewResource(R.layout.recurrencepicker_freq_item);
+ mEndSpinner.setAdapter(mEndSpinnerAdapter);
+
+ mEndCount = (EditText) mView.findViewById(R.id.endCount);
+ mEndCount.addTextChangedListener(new minMaxTextWatcher(1, COUNT_DEFAULT, COUNT_MAX) {
+ @Override
+ void onChange(int v) {
+ mModel.endCount = v;
+ }
+ });
+ mEndDateTextView = (Button) mView.findViewById(R.id.endDate);
+ mEndDateTextView.setOnClickListener(this);
+ if (mModel.endDate == null) {
+ mModel.endDate = new Time(mTime);
+ switch (mModel.freq) {
+ case Model.FREQ_NONE:
+ case Model.FREQ_DAILY:
+ case Model.FREQ_WEEKLY:
+ mModel.endDate.month += 1;
+ break;
+ case Model.FREQ_MONTHLY:
+ mModel.endDate.month += 3;
+ break;
+ case Model.FREQ_YEARLY:
+ mModel.endDate.year += 3;
+ break;
+ }
+ mModel.endDate.normalize(false);
+ }
+
+ mWeekGroup = (LinearLayout) mView.findViewById(R.id.weekGroup);
+
+ mOrdinalArray = mResources.getStringArray(R.array.ordinal_labels);
+
+ // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7
+ String[] dayOfWeekString = new DateFormatSymbols().getWeekdays();
+ mDayOfWeekString = new String[7];
+ for (int i = 0; i < 7; i++) {
+ mDayOfWeekString[i] = dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[i]];
+ }
+
+ // In Time.java day of week order e.g. Sun = 0
+ int idx = Utils.getFirstDayOfWeek(getActivity());
+
+ // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7
+ dayOfWeekString = new DateFormatSymbols().getShortWeekdays();
+
+ for (int i = 0; i < 7; i++) {
+ mWeekByDayButtons[idx] = (ToggleButton) mWeekGroup.getChildAt(i);
+ mWeekByDayButtons[idx].setTextOff(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
+ mWeekByDayButtons[idx].setTextOn(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
+ mWeekByDayButtons[idx].setOnCheckedChangeListener(this);
+
+ if (++idx >= 7) {
+ idx = 0;
+ }
+ }
+
+ mMonthGroup = (LinearLayout) mView.findViewById(R.id.monthGroup);
+ mMonthRepeatByRadioGroup = (RadioGroup) mView.findViewById(R.id.monthGroup);
+ mMonthRepeatByRadioGroup.setOnCheckedChangeListener(this);
+ mMonthRepeatByNthDayOfWeek = (RadioButton) mView
+ .findViewById(R.id.repeatMonthlyByNthDayOfTheWeek);
+
+ mDone = (Button) mView.findViewById(R.id.done);
+ mDone.setOnClickListener(this);
+
+ updateDialog();
+ return mView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(BUNDLE_MODEL, mModel);
+ }
+
+ public void updateDialog() {
+ // Interval
+ // Checking before setting because this causes infinite recursion
+ // in afterTextWatcher
+ final String intervalStr = Integer.toString(mModel.interval);
+ if (!intervalStr.equals(mInterval.getText().toString())) {
+ mInterval.setText(intervalStr);
+ }
+
+ mFreqSpinner.setSelection(mModel.freq + 1); // FREQ_* starts at -1
+ mWeekGroup.setVisibility(mModel.freq == Model.FREQ_WEEKLY ? View.VISIBLE : View.GONE);
+ mMonthGroup.setVisibility(mModel.freq == Model.FREQ_MONTHLY ? View.VISIBLE : View.GONE);
+
+ if (mModel.freq == Model.FREQ_NONE) {
+ mIntervalGroup.setVisibility(View.INVISIBLE);
+ mEndGroup.setVisibility(View.INVISIBLE);
+ } else {
+ mIntervalGroup.setVisibility(View.VISIBLE);
+ mEndGroup.setVisibility(View.VISIBLE);
+
+ switch (mModel.freq) {
+ case Model.FREQ_DAILY:
+ updateIntervalText(R.string.recurrence_interval_daily);
+ break;
+
+ case Model.FREQ_WEEKLY:
+ updateIntervalText(R.string.recurrence_interval_weekly);
+
+ int count = 0;
+ for (int i = 0; i < 7; i++) {
+ mWeekByDayButtons[i].setChecked(mModel.weeklyByDayOfWeek[i]);
+ if (mModel.weeklyByDayOfWeek[i]) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ mModel.weeklyByDayOfWeek[mTime.weekDay] = true;
+ mWeekByDayButtons[mTime.weekDay].setChecked(true);
+ }
+ break;
+
+ case Model.FREQ_MONTHLY:
+ updateIntervalText(R.string.recurrence_interval_monthly);
+
+ if (mModel.monthlyRepeat == Model.MONTHLY_BY_DATE) {
+ mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfMonth);
+ } else if (mModel.monthlyRepeat == Model.MONTHLY_BY_NTH_DAY_OF_WEEK) {
+ mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfTheWeek);
+ }
+
+ if (mMonthRepeatByDayOfWeekStr == null) {
+ if (mModel.monthlyByNthDayOfWeek == 0) {
+ mModel.monthlyByNthDayOfWeek = (mTime.monthDay + 6) / 7;
+ mModel.monthlyByDayOfWeek = mTime.weekDay;
+ }
+
+ mMonthRepeatByDayOfWeekStr = mResources.getString(
+ R.string.recurrence_month_pattern_by_day_of_week,
+ mOrdinalArray[mModel.monthlyByNthDayOfWeek - 1],
+ mDayOfWeekString[mModel.monthlyByDayOfWeek]);
+ mMonthRepeatByNthDayOfWeek.setText(mMonthRepeatByDayOfWeekStr);
+ }
+ break;
+
+ case Model.FREQ_YEARLY:
+ updateIntervalText(R.string.recurrence_interval_yearly);
+ break;
+ }
+
+ mEndSpinner.setSelection(mModel.end);
+ if (mModel.end == Model.END_BY_DATE) {
+ final String dateStr = DateUtils.formatDateTime(getActivity(),
+ mModel.endDate.toMillis(false), DateUtils.FORMAT_NUMERIC_DATE);
+ mEndDateTextView.setText(dateStr);
+ } else if (mModel.end == Model.END_BY_COUNT) {
+ // Checking before setting because this causes infinite
+ // recursion
+ // in afterTextWatcher
+ final String countStr = Integer.toString(mModel.endCount);
+ if (!countStr.equals(mEndCount.getText().toString())) {
+ mEndCount.setText(countStr);
+ }
+ }
+ }
+
+ // TODO Update title with pretty rrule
+ if (getDialog() != null) {
+ getDialog().setTitle(R.string.recurrence_dialog_title);
+
+ // copyModelToEventRecurrence(mModel, mRecurrence);
+ // String title =
+ // EventRecurrenceFormatter.getRepeatString(getActivity(),
+ // mResources,
+ // mRecurrence, false);
+ // if (title != null) {
+ // getDialog().setTitle(title);
+ // } else {
+ // getDialog().setTitle(R.string.recurrence_dialog_title);
+ // }
+ }
+ // doToast();
+ }
+
+ private void doToast() {
+ Log.e(TAG, "Model = " + mModel.toString());
+ String rrule;
+ if (mModel.freq == Model.FREQ_NONE) {
+ rrule = "Not repeating";
+ } else {
+ copyModelToEventRecurrence(mModel, mRecurrence);
+ rrule = mRecurrence.toString();
+ }
+
+ if (mToast != null) {
+ mToast.cancel();
+ }
+ mToast = Toast.makeText(getActivity(), rrule,
+ Toast.LENGTH_LONG);
+ mToast.show();
+ }
+
+ // TODO Test and update for Right-to-Left
+ private void updateIntervalText(int intervalStringId) {
+ final String INTERVAL_COUNT_MARKER = "%d";
+ String intervalString = mResources.getString(intervalStringId);
+ int markerStart = intervalString.indexOf(INTERVAL_COUNT_MARKER);
+
+ if (markerStart != -1) {
+ if (markerStart == 0) {
+ mIntervalPreText.setText("");
+ } else {
+ int postTextStart = markerStart + INTERVAL_COUNT_MARKER.length();
+ if (intervalString.charAt(postTextStart) == ' ') {
+ postTextStart++;
+ }
+ mIntervalPostText.setText(intervalString.subSequence(postTextStart,
+ intervalString.length()));
+
+ if (intervalString.charAt(markerStart - 1) == ' ') {
+ markerStart--;
+ }
+ mIntervalPreText.setText(intervalString.subSequence(0, markerStart));
+ }
+ }
+ }
+
+ // Implements OnItemSelectedListener interface
+ // Freq spinner
+ // End spinner
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (parent == mFreqSpinner) {
+ mModel.freq = position - 1; // FREQ_* starts at -1; Spinner starts at 0
+ } else if (parent == mEndSpinner) {
+ switch (position) {
+ case Model.END_NEVER:
+ mModel.end = Model.END_NEVER;
+ break;
+ case Model.END_BY_DATE:
+ mModel.end = Model.END_BY_DATE;
+ break;
+ case Model.END_BY_COUNT:
+ mModel.end = Model.END_BY_COUNT;
+
+ if (mModel.endCount <= 1) {
+ mModel.endCount = 1;
+ } else if (mModel.endCount > COUNT_MAX) {
+ mModel.endCount = COUNT_MAX;
+ }
+ break;
+ }
+ mEndCount.setVisibility(mModel.end == Model.END_BY_COUNT ? View.VISIBLE
+ : View.GONE);
+ mEndDateTextView.setVisibility(mModel.end == Model.END_BY_DATE ? View.VISIBLE
+ : View.GONE);
+ }
+ updateDialog();
+ }
+
+ // Implements OnItemSelectedListener interface
+ @Override
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+
+ static public class DatePickerDialogFragment extends DialogFragment
+ implements DatePickerDialog.OnDateSetListener {
+ private OnDateSetListener mOnDateSetListener;
+ private DatePickerDialog mDialog;
+
+ public DatePickerDialogFragment() {
+ super();
+ }
+
+ public void setOnDateSetListener(DatePickerDialog.OnDateSetListener l) {
+ mOnDateSetListener = l;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle b) {
+ int year, month, day;
+
+ if (b == null) {
+ b = getArguments();
+ }
+
+ if (b == null) {
+ Time t = new Time(); // TODO timezone?
+ t.setToNow();
+ t.month += 3;
+ t.normalize(false);
+
+ year = t.year;
+ month = t.month;
+ day = t.monthDay;
+ } else {
+ year = b.getInt(BUNDLE_END_YEAR);
+ month = b.getInt(BUNDLE_END_MONTH);
+ day = b.getInt(BUNDLE_END_DAY);
+ }
+
+ mDialog = new DatePickerDialog(getActivity(), this, year,
+ month, day);
+
+ return mDialog;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
+
+ if (mOnDateSetListener != null) {
+ mOnDateSetListener.onDateSet(null, year, month, dayOfMonth);
+ }
+ }
+ }
+
+ @Override
+ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+ if (mModel.endDate == null) {
+ mModel.endDate = new Time(mTime.timezone);
+ mModel.endDate.hour = mModel.endDate.minute = mModel.endDate.second = 0;
+ }
+ mModel.endDate.year = year;
+ mModel.endDate.month = monthOfYear;
+ mModel.endDate.monthDay = dayOfMonth;
+ mModel.endDate.normalize(false);
+ updateDialog();
+ }
+
+ // Implements OnCheckedChangeListener interface
+ // Week repeat by day of week
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ int itemIdx = -1;
+ int checkedItems = 0;
+ for (int i = 0; i < 7; i++) {
+ if (itemIdx == -1 && buttonView == mWeekByDayButtons[i]) {
+ itemIdx = i;
+ mModel.weeklyByDayOfWeek[i] = isChecked;
+ }
+
+ if (mModel.weeklyByDayOfWeek[i]) {
+ checkedItems++;
+ }
+ }
+
+ // Re-enable item if nothing was enabled.
+ if (checkedItems == 0 && itemIdx != -1) {
+ buttonView.setChecked(true);
+ mModel.weeklyByDayOfWeek[itemIdx] = true;
+ }
+
+ updateDialog();
+ }
+
+ // Implements android.widget.RadioGroup.OnCheckedChangeListener interface
+ // Month repeat by radio buttons
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ if (checkedId == R.id.repeatMonthlyByNthDayOfMonth) {
+ mModel.monthlyRepeat = Model.MONTHLY_BY_DATE;
+ } else if (checkedId == R.id.repeatMonthlyByNthDayOfTheWeek) {
+ mModel.monthlyRepeat = Model.MONTHLY_BY_NTH_DAY_OF_WEEK;
+ }
+ updateDialog();
+ }
+
+ // Implements OnClickListener interface
+ // EndDate button
+ // Done button
+ @Override
+ public void onClick(View v) {
+ if (mEndDateTextView == v) {
+ Bundle b = new Bundle();
+ b.putInt(BUNDLE_END_YEAR, mModel.endDate.year);
+ b.putInt(BUNDLE_END_MONTH, mModel.endDate.month);
+ b.putInt(BUNDLE_END_DAY, mModel.endDate.monthDay);
+
+ DatePickerDialogFragment dpdf = (DatePickerDialogFragment) getFragmentManager()
+ .findFragmentByTag(FRAG_TAG_DATE_PICKER);
+ if (dpdf != null) {
+ dpdf.dismiss();
+ }
+ dpdf = new DatePickerDialogFragment();
+ dpdf.setArguments(b);
+ dpdf.setOnDateSetListener(this);
+ dpdf.show(getFragmentManager(), FRAG_TAG_DATE_PICKER);
+ } else if (mDone == v) {
+ String rrule;
+ if (mModel.freq == Model.FREQ_NONE) {
+ rrule = null;
+ } else {
+ copyModelToEventRecurrence(mModel, mRecurrence);
+ rrule = mRecurrence.toString();
+ }
+
+ mRecurrenceSetListener.onRecurrenceSet(rrule);
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ DatePickerDialogFragment dialogFrag = (DatePickerDialogFragment) getFragmentManager()
+ .findFragmentByTag(FRAG_TAG_DATE_PICKER);
+ if (dialogFrag != null) {
+ dialogFrag.setOnDateSetListener(this);
+ }
+ }
+
+ public interface OnRecurrenceSetListener {
+ void onRecurrenceSet(String rrule);
+ }
+
+ public void setOnRecurrenceSetListener(OnRecurrenceSetListener l) {
+ mRecurrenceSetListener = l;
+ }
+
+ // TODO handle the case where user deletes all the text.
+
+ // @Override
+ // public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // if (v == mInterval) {
+ // int interval;
+ // try {
+ // interval = Integer.parseInt(mInterval.getText().toString());
+ // } catch (NumberFormatException e) {
+ // interval = INTERVAL_DEFAULT;
+ // }
+ // mModel.interval = interval;
+ // } else if (v == mEndCount) {
+ // int count;
+ // try {
+ // count = Integer.parseInt(mEndCount.getText().toString());
+ // } catch (NumberFormatException e) {
+ // count = COUNT_DEFAULT;
+ // }
+ // mModel.endCount = count;
+ // }
+ // doToast();
+ // return false;
+ // }
+}
diff --git a/src/com/android/calendar/recurrencepicker/WeekButton.java b/src/com/android/calendar/recurrencepicker/WeekButton.java
new file mode 100644
index 00000000..fd17834a
--- /dev/null
+++ b/src/com/android/calendar/recurrencepicker/WeekButton.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 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.recurrencepicker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class WeekButton extends android.widget.ToggleButton {
+
+ private static int mWidth;
+
+ public WeekButton(Context context) {
+ super(context);
+ }
+
+ public WeekButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WeekButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public static void setSuggestedWidth(int w) {
+ mWidth = w;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int w = getMeasuredWidth();
+ final int wMode = View.MeasureSpec.getMode(w);
+ if (wMode != MeasureSpec.EXACTLY) {
+ final int maxWidth = View.MeasureSpec.getSize(widthMeasureSpec);
+ w = mWidth;
+ if (wMode == MeasureSpec.AT_MOST && w > maxWidth) {
+ w = maxWidth;
+ }
+ }
+ setMeasuredDimension(w, getMeasuredHeight());
+ }
+}