1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calendar.recurrencepicker;
18 
19 import android.app.Activity;
20 import android.app.DialogFragment;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.os.Bundle;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.text.Editable;
28 import android.text.TextUtils;
29 import android.text.TextWatcher;
30 import android.text.format.DateUtils;
31 import android.text.format.Time;
32 import android.util.Log;
33 import android.util.TimeFormatException;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.View.OnClickListener;
37 import android.view.ViewGroup;
38 import android.view.ViewGroup.LayoutParams;
39 import android.view.Window;
40 import android.widget.AdapterView;
41 import android.widget.AdapterView.OnItemSelectedListener;
42 import android.widget.ArrayAdapter;
43 import android.widget.Button;
44 import android.widget.CompoundButton;
45 import android.widget.CompoundButton.OnCheckedChangeListener;
46 import android.widget.EditText;
47 import android.widget.LinearLayout;
48 import android.widget.RadioButton;
49 import android.widget.RadioGroup;
50 import android.widget.Spinner;
51 import android.widget.Switch;
52 import android.widget.TableLayout;
53 import android.widget.TextView;
54 import android.widget.Toast;
55 import android.widget.ToggleButton;
56 
57 import com.android.calendar.R;
58 import com.android.calendar.Utils;
59 import com.android.calendarcommon2.EventRecurrence;
60 import com.android.datetimepicker.date.DatePickerDialog;
61 
62 import java.text.DateFormatSymbols;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Calendar;
66 
67 public class RecurrencePickerDialog extends DialogFragment implements OnItemSelectedListener,
68         OnCheckedChangeListener, OnClickListener,
69         android.widget.RadioGroup.OnCheckedChangeListener, DatePickerDialog.OnDateSetListener {
70 
71     private static final String TAG = "RecurrencePickerDialog";
72 
73     // in dp's
74     private static final int MIN_SCREEN_WIDTH_FOR_SINGLE_ROW_WEEK = 450;
75 
76     // Update android:maxLength in EditText as needed
77     private static final int INTERVAL_MAX = 99;
78     private static final int INTERVAL_DEFAULT = 1;
79     // Update android:maxLength in EditText as needed
80     private static final int COUNT_MAX = 730;
81     private static final int COUNT_DEFAULT = 5;
82 
83     // Special cases in monthlyByNthDayOfWeek
84     private static final int FIFTH_WEEK_IN_A_MONTH = 5;
85     private static final int LAST_NTH_DAY_OF_WEEK = -1;
86 
87     private DatePickerDialog mDatePickerDialog;
88 
89     private class RecurrenceModel implements Parcelable {
90 
91         // Should match EventRecurrence.DAILY, etc
92         static final int FREQ_DAILY = 0;
93         static final int FREQ_WEEKLY = 1;
94         static final int FREQ_MONTHLY = 2;
95         static final int FREQ_YEARLY = 3;
96 
97         static final int END_NEVER = 0;
98         static final int END_BY_DATE = 1;
99         static final int END_BY_COUNT = 2;
100 
101         static final int MONTHLY_BY_DATE = 0;
102         static final int MONTHLY_BY_NTH_DAY_OF_WEEK = 1;
103 
104         static final int STATE_NO_RECURRENCE = 0;
105         static final int STATE_RECURRENCE = 1;
106 
107         int recurrenceState;
108 
109         /**
110          * FREQ: Repeat pattern
111          *
112          * @see FREQ_DAILY
113          * @see FREQ_WEEKLY
114          * @see FREQ_MONTHLY
115          * @see FREQ_YEARLY
116          */
117         int freq = FREQ_WEEKLY;
118 
119         /**
120          * INTERVAL: Every n days/weeks/months/years. n >= 1
121          */
122         int interval = INTERVAL_DEFAULT;
123 
124         /**
125          * UNTIL and COUNT: How does the the event end?
126          *
127          * @see END_NEVER
128          * @see END_BY_DATE
129          * @see END_BY_COUNT
130          * @see untilDate
131          * @see untilCount
132          */
133         int end;
134 
135         /**
136          * UNTIL: Date of the last recurrence. Used when until == END_BY_DATE
137          */
138         Time endDate;
139 
140         /**
141          * COUNT: Times to repeat. Use when until == END_BY_COUNT
142          */
143         int endCount = COUNT_DEFAULT;
144 
145         /**
146          * BYDAY: Days of the week to be repeated. Sun = 0, Mon = 1, etc
147          */
148         boolean[] weeklyByDayOfWeek = new boolean[7];
149 
150         /**
151          * BYDAY AND BYMONTHDAY: How to repeat monthly events? Same date of the
152          * month or Same nth day of week.
153          *
154          * @see MONTHLY_BY_DATE
155          * @see MONTHLY_BY_NTH_DAY_OF_WEEK
156          */
157         int monthlyRepeat;
158 
159         /**
160          * Day of the month to repeat. Used when monthlyRepeat ==
161          * MONTHLY_BY_DATE
162          */
163         int monthlyByMonthDay;
164 
165         /**
166          * Day of the week to repeat. Used when monthlyRepeat ==
167          * MONTHLY_BY_NTH_DAY_OF_WEEK
168          */
169         int monthlyByDayOfWeek;
170 
171         /**
172          * Nth day of the week to repeat. Used when monthlyRepeat ==
173          * MONTHLY_BY_NTH_DAY_OF_WEEK 0=undefined, -1=Last, 1=1st, 2=2nd, ..., 5=5th
174          *
175          * We support 5th, just to handle backwards capabilities with old bug, but it
176          * gets converted to -1 once edited.
177          */
178         int monthlyByNthDayOfWeek;
179 
180         /*
181          * (generated method)
182          */
183         @Override
toString()184         public String toString() {
185             return "Model [freq=" + freq + ", interval=" + interval + ", end=" + end + ", endDate="
186                     + endDate + ", endCount=" + endCount + ", weeklyByDayOfWeek="
187                     + Arrays.toString(weeklyByDayOfWeek) + ", monthlyRepeat=" + monthlyRepeat
188                     + ", monthlyByMonthDay=" + monthlyByMonthDay + ", monthlyByDayOfWeek="
189                     + monthlyByDayOfWeek + ", monthlyByNthDayOfWeek=" + monthlyByNthDayOfWeek + "]";
190         }
191 
192         @Override
describeContents()193         public int describeContents() {
194             return 0;
195         }
196 
RecurrenceModel()197         public RecurrenceModel() {
198         }
199 
200         @Override
writeToParcel(Parcel dest, int flags)201         public void writeToParcel(Parcel dest, int flags) {
202             dest.writeInt(freq);
203             dest.writeInt(interval);
204             dest.writeInt(end);
205             dest.writeInt(endDate.year);
206             dest.writeInt(endDate.month);
207             dest.writeInt(endDate.monthDay);
208             dest.writeInt(endCount);
209             dest.writeBooleanArray(weeklyByDayOfWeek);
210             dest.writeInt(monthlyRepeat);
211             dest.writeInt(monthlyByMonthDay);
212             dest.writeInt(monthlyByDayOfWeek);
213             dest.writeInt(monthlyByNthDayOfWeek);
214             dest.writeInt(recurrenceState);
215         }
216     }
217 
218     class minMaxTextWatcher implements TextWatcher {
219         private int mMin;
220         private int mMax;
221         private int mDefault;
222 
minMaxTextWatcher(int min, int defaultInt, int max)223         public minMaxTextWatcher(int min, int defaultInt, int max) {
224             mMin = min;
225             mMax = max;
226             mDefault = defaultInt;
227         }
228 
229         @Override
afterTextChanged(Editable s)230         public void afterTextChanged(Editable s) {
231 
232             boolean updated = false;
233             int value;
234             try {
235                 value = Integer.parseInt(s.toString());
236             } catch (NumberFormatException e) {
237                 value = mDefault;
238             }
239 
240             if (value < mMin) {
241                 value = mMin;
242                 updated = true;
243             } else if (value > mMax) {
244                 updated = true;
245                 value = mMax;
246             }
247 
248             // Update UI
249             if (updated) {
250                 s.clear();
251                 s.append(Integer.toString(value));
252             }
253 
254             updateDoneButtonState();
255             onChange(value);
256         }
257 
258         /** Override to be called after each key stroke */
onChange(int value)259         void onChange(int value) {
260         }
261 
262         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)263         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
264         }
265 
266         @Override
onTextChanged(CharSequence s, int start, int before, int count)267         public void onTextChanged(CharSequence s, int start, int before, int count) {
268         }
269     }
270 
271     private Resources mResources;
272     private EventRecurrence mRecurrence = new EventRecurrence();
273     private Time mTime = new Time(); // TODO timezone?
274     private RecurrenceModel mModel = new RecurrenceModel();
275     private Toast mToast;
276 
277     private final int[] TIME_DAY_TO_CALENDAR_DAY = new int[] {
278             Calendar.SUNDAY,
279             Calendar.MONDAY,
280             Calendar.TUESDAY,
281             Calendar.WEDNESDAY,
282             Calendar.THURSDAY,
283             Calendar.FRIDAY,
284             Calendar.SATURDAY,
285     };
286 
287     // Call mStringBuilder.setLength(0) before formatting any string or else the
288     // formatted text will accumulate.
289     // private final StringBuilder mStringBuilder = new StringBuilder();
290     // private Formatter mFormatter = new Formatter(mStringBuilder);
291 
292     private View mView;
293 
294     private Spinner mFreqSpinner;
295     private static final int[] mFreqModelToEventRecurrence = {
296             EventRecurrence.DAILY,
297             EventRecurrence.WEEKLY,
298             EventRecurrence.MONTHLY,
299             EventRecurrence.YEARLY
300     };
301 
302     public static final String BUNDLE_START_TIME_MILLIS = "bundle_event_start_time";
303     public static final String BUNDLE_TIME_ZONE = "bundle_event_time_zone";
304     public static final String BUNDLE_RRULE = "bundle_event_rrule";
305 
306     private static final String BUNDLE_MODEL = "bundle_model";
307     private static final String BUNDLE_END_COUNT_HAS_FOCUS = "bundle_end_count_has_focus";
308 
309     private static final String FRAG_TAG_DATE_PICKER = "tag_date_picker_frag";
310 
311     private Switch mRepeatSwitch;
312 
313     private EditText mInterval;
314     private TextView mIntervalPreText;
315     private TextView mIntervalPostText;
316 
317     private int mIntervalResId = -1;
318 
319     private Spinner mEndSpinner;
320     private TextView mEndDateTextView;
321     private EditText mEndCount;
322     private TextView mPostEndCount;
323     private boolean mHidePostEndCount;
324 
325     private ArrayList<CharSequence> mEndSpinnerArray = new ArrayList<CharSequence>(3);
326     private EndSpinnerAdapter mEndSpinnerAdapter;
327     private String mEndNeverStr;
328     private String mEndDateLabel;
329     private String mEndCountLabel;
330 
331     /** Hold toggle buttons in the order per user's first day of week preference */
332     private LinearLayout mWeekGroup;
333     private LinearLayout mWeekGroup2;
334     // Sun = 0
335     private ToggleButton[] mWeekByDayButtons = new ToggleButton[7];
336     /** A double array of Strings to hold the 7x5 list of possible strings of the form:
337      *  "on every [Nth] [DAY_OF_WEEK]", e.g. "on every second Monday",
338      *  where [Nth] can be [first, second, third, fourth, last] */
339     private String[][] mMonthRepeatByDayOfWeekStrs;
340 
341     private LinearLayout mMonthGroup;
342     private RadioGroup mMonthRepeatByRadioGroup;
343     private RadioButton mRepeatMonthlyByNthDayOfWeek;
344     private RadioButton mRepeatMonthlyByNthDayOfMonth;
345     private String mMonthRepeatByDayOfWeekStr;
346 
347     private Button mDone;
348 
349     private OnRecurrenceSetListener mRecurrenceSetListener;
350 
RecurrencePickerDialog()351     public RecurrencePickerDialog() {
352     }
353 
isSupportedMonthlyByNthDayOfWeek(int num)354     static public boolean isSupportedMonthlyByNthDayOfWeek(int num) {
355         // We only support monthlyByNthDayOfWeek when it is greater then 0 but less then 5.
356         // Or if -1 when it is the last monthly day of the week.
357         return (num > 0 && num <= FIFTH_WEEK_IN_A_MONTH) || num == LAST_NTH_DAY_OF_WEEK;
358     }
359 
canHandleRecurrenceRule(EventRecurrence er)360     static public boolean canHandleRecurrenceRule(EventRecurrence er) {
361         switch (er.freq) {
362             case EventRecurrence.DAILY:
363             case EventRecurrence.MONTHLY:
364             case EventRecurrence.YEARLY:
365             case EventRecurrence.WEEKLY:
366                 break;
367             default:
368                 return false;
369         }
370 
371         if (er.count > 0 && !TextUtils.isEmpty(er.until)) {
372             return false;
373         }
374 
375         // Weekly: For "repeat by day of week", the day of week to repeat is in
376         // er.byday[]
377 
378         /*
379          * Monthly: For "repeat by nth day of week" the day of week to repeat is
380          * in er.byday[] and the "nth" is stored in er.bydayNum[]. Currently we
381          * can handle only one and only in monthly
382          */
383         int numOfByDayNum = 0;
384         for (int i = 0; i < er.bydayCount; i++) {
385             if (isSupportedMonthlyByNthDayOfWeek(er.bydayNum[i])) {
386                 ++numOfByDayNum;
387             }
388         }
389 
390         if (numOfByDayNum > 1) {
391             return false;
392         }
393 
394         if (numOfByDayNum > 0 && er.freq != EventRecurrence.MONTHLY) {
395             return false;
396         }
397 
398         // The UI only handle repeat by one day of month i.e. not 9th and 10th
399         // of every month
400         if (er.bymonthdayCount > 1) {
401             return false;
402         }
403 
404         if (er.freq == EventRecurrence.MONTHLY) {
405             if (er.bydayCount > 1) {
406                 return false;
407             }
408             if (er.bydayCount > 0 && er.bymonthdayCount > 0) {
409                 return false;
410             }
411         }
412 
413         return true;
414     }
415 
416     // TODO don't lose data when getting data that our UI can't handle
copyEventRecurrenceToModel(final EventRecurrence er, RecurrenceModel model)417     static private void copyEventRecurrenceToModel(final EventRecurrence er,
418             RecurrenceModel model) {
419         // Freq:
420         switch (er.freq) {
421             case EventRecurrence.DAILY:
422                 model.freq = RecurrenceModel.FREQ_DAILY;
423                 break;
424             case EventRecurrence.MONTHLY:
425                 model.freq = RecurrenceModel.FREQ_MONTHLY;
426                 break;
427             case EventRecurrence.YEARLY:
428                 model.freq = RecurrenceModel.FREQ_YEARLY;
429                 break;
430             case EventRecurrence.WEEKLY:
431                 model.freq = RecurrenceModel.FREQ_WEEKLY;
432                 break;
433             default:
434                 throw new IllegalStateException("freq=" + er.freq);
435         }
436 
437         // Interval:
438         if (er.interval > 0) {
439             model.interval = er.interval;
440         }
441 
442         // End:
443         // End by count:
444         model.endCount = er.count;
445         if (model.endCount > 0) {
446             model.end = RecurrenceModel.END_BY_COUNT;
447         }
448 
449         // End by date:
450         if (!TextUtils.isEmpty(er.until)) {
451             if (model.endDate == null) {
452                 model.endDate = new Time();
453             }
454 
455             try {
456                 model.endDate.parse(er.until);
457             } catch (TimeFormatException e) {
458                 model.endDate = null;
459             }
460 
461             // LIMITATION: The UI can only handle END_BY_DATE or END_BY_COUNT
462             if (model.end == RecurrenceModel.END_BY_COUNT && model.endDate != null) {
463                 throw new IllegalStateException("freq=" + er.freq);
464             }
465 
466             model.end = RecurrenceModel.END_BY_DATE;
467         }
468 
469         // Weekly: repeat by day of week or Monthly: repeat by nth day of week
470         // in the month
471         Arrays.fill(model.weeklyByDayOfWeek, false);
472         if (er.bydayCount > 0) {
473             int count = 0;
474             for (int i = 0; i < er.bydayCount; i++) {
475                 int dayOfWeek = EventRecurrence.day2TimeDay(er.byday[i]);
476                 model.weeklyByDayOfWeek[dayOfWeek] = true;
477 
478                 if (model.freq == RecurrenceModel.FREQ_MONTHLY &&
479                         isSupportedMonthlyByNthDayOfWeek(er.bydayNum[i])) {
480                     // LIMITATION: Can handle only (one) weekDayNum in nth or last and only
481                     // when
482                     // monthly
483                     model.monthlyByDayOfWeek = dayOfWeek;
484                     model.monthlyByNthDayOfWeek = er.bydayNum[i];
485                     model.monthlyRepeat = RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK;
486                     count++;
487                 }
488             }
489 
490             if (model.freq == RecurrenceModel.FREQ_MONTHLY) {
491                 if (er.bydayCount != 1) {
492                     // Can't handle 1st Monday and 2nd Wed
493                     throw new IllegalStateException("Can handle only 1 byDayOfWeek in monthly");
494                 }
495                 if (count != 1) {
496                     throw new IllegalStateException(
497                             "Didn't specify which nth day of week to repeat for a monthly");
498                 }
499             }
500         }
501 
502         // Monthly by day of month
503         if (model.freq == RecurrenceModel.FREQ_MONTHLY) {
504             if (er.bymonthdayCount == 1) {
505                 if (model.monthlyRepeat == RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK) {
506                     throw new IllegalStateException(
507                             "Can handle only by monthday or by nth day of week, not both");
508                 }
509                 model.monthlyByMonthDay = er.bymonthday[0];
510                 model.monthlyRepeat = RecurrenceModel.MONTHLY_BY_DATE;
511             } else if (er.bymonthCount > 1) {
512                 // LIMITATION: Can handle only one month day
513                 throw new IllegalStateException("Can handle only one bymonthday");
514             }
515         }
516     }
517 
copyModelToEventRecurrence(final RecurrenceModel model, EventRecurrence er)518     static private void copyModelToEventRecurrence(final RecurrenceModel model,
519             EventRecurrence er) {
520         if (model.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) {
521             throw new IllegalStateException("There's no recurrence");
522         }
523 
524         // Freq
525         er.freq = mFreqModelToEventRecurrence[model.freq];
526 
527         // Interval
528         if (model.interval <= 1) {
529             er.interval = 0;
530         } else {
531             er.interval = model.interval;
532         }
533 
534         // End
535         switch (model.end) {
536             case RecurrenceModel.END_BY_DATE:
537                 if (model.endDate != null) {
538                     model.endDate.switchTimezone(Time.TIMEZONE_UTC);
539                     model.endDate.normalize(false);
540                     er.until = model.endDate.format2445();
541                     er.count = 0;
542                 } else {
543                     throw new IllegalStateException("end = END_BY_DATE but endDate is null");
544                 }
545                 break;
546             case RecurrenceModel.END_BY_COUNT:
547                 er.count = model.endCount;
548                 er.until = null;
549                 if (er.count <= 0) {
550                     throw new IllegalStateException("count is " + er.count);
551                 }
552                 break;
553             default:
554                 er.count = 0;
555                 er.until = null;
556                 break;
557         }
558 
559         // Weekly && monthly repeat patterns
560         er.bydayCount = 0;
561         er.bymonthdayCount = 0;
562 
563         switch (model.freq) {
564             case RecurrenceModel.FREQ_MONTHLY:
565                 if (model.monthlyRepeat == RecurrenceModel.MONTHLY_BY_DATE) {
566                     if (model.monthlyByMonthDay > 0) {
567                         if (er.bymonthday == null || er.bymonthdayCount < 1) {
568                             er.bymonthday = new int[1];
569                         }
570                         er.bymonthday[0] = model.monthlyByMonthDay;
571                         er.bymonthdayCount = 1;
572                     }
573                 } else if (model.monthlyRepeat == RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK) {
574                     if (!isSupportedMonthlyByNthDayOfWeek(model.monthlyByNthDayOfWeek)) {
575                         throw new IllegalStateException("month repeat by nth week but n is "
576                                 + model.monthlyByNthDayOfWeek);
577                     }
578                     int count = 1;
579                     if (er.bydayCount < count || er.byday == null || er.bydayNum == null) {
580                         er.byday = new int[count];
581                         er.bydayNum = new int[count];
582                     }
583                     er.bydayCount = count;
584                     er.byday[0] = EventRecurrence.timeDay2Day(model.monthlyByDayOfWeek);
585                     er.bydayNum[0] = model.monthlyByNthDayOfWeek;
586                 }
587                 break;
588             case RecurrenceModel.FREQ_WEEKLY:
589                 int count = 0;
590                 for (int i = 0; i < 7; i++) {
591                     if (model.weeklyByDayOfWeek[i]) {
592                         count++;
593                     }
594                 }
595 
596                 if (er.bydayCount < count || er.byday == null || er.bydayNum == null) {
597                     er.byday = new int[count];
598                     er.bydayNum = new int[count];
599                 }
600                 er.bydayCount = count;
601 
602                 for (int i = 6; i >= 0; i--) {
603                     if (model.weeklyByDayOfWeek[i]) {
604                         er.bydayNum[--count] = 0;
605                         er.byday[count] = EventRecurrence.timeDay2Day(i);
606                     }
607                 }
608                 break;
609         }
610 
611         if (!canHandleRecurrenceRule(er)) {
612             throw new IllegalStateException("UI generated recurrence that it can't handle. ER:"
613                     + er.toString() + " Model: " + model.toString());
614         }
615     }
616 
617     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)618     public View onCreateView(LayoutInflater inflater, ViewGroup container,
619             Bundle savedInstanceState) {
620         mRecurrence.wkst = EventRecurrence.timeDay2Day(Utils.getFirstDayOfWeek(getActivity()));
621 
622         getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
623 
624         boolean endCountHasFocus = false;
625         if (savedInstanceState != null) {
626             RecurrenceModel m = (RecurrenceModel) savedInstanceState.get(BUNDLE_MODEL);
627             if (m != null) {
628                 mModel = m;
629             }
630             endCountHasFocus = savedInstanceState.getBoolean(BUNDLE_END_COUNT_HAS_FOCUS);
631         } else {
632             Bundle b = getArguments();
633             if (b != null) {
634                 mTime.set(b.getLong(BUNDLE_START_TIME_MILLIS));
635 
636                 String tz = b.getString(BUNDLE_TIME_ZONE);
637                 if (!TextUtils.isEmpty(tz)) {
638                     mTime.timezone = tz;
639                 }
640                 mTime.normalize(false);
641 
642                 // Time days of week: Sun=0, Mon=1, etc
643                 mModel.weeklyByDayOfWeek[mTime.weekDay] = true;
644                 String rrule = b.getString(BUNDLE_RRULE);
645                 if (!TextUtils.isEmpty(rrule)) {
646                     mModel.recurrenceState = RecurrenceModel.STATE_RECURRENCE;
647                     mRecurrence.parse(rrule);
648                     copyEventRecurrenceToModel(mRecurrence, mModel);
649                     // Leave today's day of week as checked by default in weekly view.
650                     if (mRecurrence.bydayCount == 0) {
651                         mModel.weeklyByDayOfWeek[mTime.weekDay] = true;
652                     }
653                 }
654 
655             } else {
656                 mTime.setToNow();
657             }
658         }
659 
660         mResources = getResources();
661         mView = inflater.inflate(R.layout.recurrencepicker, container, true);
662 
663         final Activity activity = getActivity();
664         final Configuration config = activity.getResources().getConfiguration();
665 
666         mRepeatSwitch = (Switch) mView.findViewById(R.id.repeat_switch);
667         mRepeatSwitch.setChecked(mModel.recurrenceState == RecurrenceModel.STATE_RECURRENCE);
668         mRepeatSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
669 
670             @Override
671             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
672                 mModel.recurrenceState = isChecked ? RecurrenceModel.STATE_RECURRENCE
673                         : RecurrenceModel.STATE_NO_RECURRENCE;
674                 togglePickerOptions();
675             }
676         });
677 
678         mFreqSpinner = (Spinner) mView.findViewById(R.id.freqSpinner);
679         mFreqSpinner.setOnItemSelectedListener(this);
680         ArrayAdapter<CharSequence> freqAdapter = ArrayAdapter.createFromResource(getActivity(),
681                 R.array.recurrence_freq, R.layout.recurrencepicker_freq_item);
682         freqAdapter.setDropDownViewResource(R.layout.recurrencepicker_freq_item);
683         mFreqSpinner.setAdapter(freqAdapter);
684 
685         mInterval = (EditText) mView.findViewById(R.id.interval);
686         mInterval.addTextChangedListener(new minMaxTextWatcher(1, INTERVAL_DEFAULT, INTERVAL_MAX) {
687             @Override
688             void onChange(int v) {
689                 if (mIntervalResId != -1 && mInterval.getText().toString().length() > 0) {
690                     mModel.interval = v;
691                     updateIntervalText();
692                     mInterval.requestLayout();
693                 }
694             }
695         });
696         mIntervalPreText = (TextView) mView.findViewById(R.id.intervalPreText);
697         mIntervalPostText = (TextView) mView.findViewById(R.id.intervalPostText);
698 
699         mEndNeverStr = mResources.getString(R.string.recurrence_end_continously);
700         mEndDateLabel = mResources.getString(R.string.recurrence_end_date_label);
701         mEndCountLabel = mResources.getString(R.string.recurrence_end_count_label);
702 
703         mEndSpinnerArray.add(mEndNeverStr);
704         mEndSpinnerArray.add(mEndDateLabel);
705         mEndSpinnerArray.add(mEndCountLabel);
706         mEndSpinner = (Spinner) mView.findViewById(R.id.endSpinner);
707         mEndSpinner.setOnItemSelectedListener(this);
708         mEndSpinnerAdapter = new EndSpinnerAdapter(getActivity(), mEndSpinnerArray,
709                 R.layout.recurrencepicker_freq_item, R.layout.recurrencepicker_end_text);
710         mEndSpinnerAdapter.setDropDownViewResource(R.layout.recurrencepicker_freq_item);
711         mEndSpinner.setAdapter(mEndSpinnerAdapter);
712 
713         mEndCount = (EditText) mView.findViewById(R.id.endCount);
714         mEndCount.addTextChangedListener(new minMaxTextWatcher(1, COUNT_DEFAULT, COUNT_MAX) {
715             @Override
716             void onChange(int v) {
717                 if (mModel.endCount != v) {
718                     mModel.endCount = v;
719                     updateEndCountText();
720                     mEndCount.requestLayout();
721                 }
722             }
723         });
724         mPostEndCount = (TextView) mView.findViewById(R.id.postEndCount);
725 
726         mEndDateTextView = (TextView) mView.findViewById(R.id.endDate);
727         mEndDateTextView.setOnClickListener(this);
728         if (mModel.endDate == null) {
729             mModel.endDate = new Time(mTime);
730             switch (mModel.freq) {
731                 case RecurrenceModel.FREQ_DAILY:
732                 case RecurrenceModel.FREQ_WEEKLY:
733                     mModel.endDate.month += 1;
734                     break;
735                 case RecurrenceModel.FREQ_MONTHLY:
736                     mModel.endDate.month += 3;
737                     break;
738                 case RecurrenceModel.FREQ_YEARLY:
739                     mModel.endDate.year += 3;
740                     break;
741             }
742             mModel.endDate.normalize(false);
743         }
744 
745         mWeekGroup = (LinearLayout) mView.findViewById(R.id.weekGroup);
746         mWeekGroup2 = (LinearLayout) mView.findViewById(R.id.weekGroup2);
747 
748         // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7
749         String[] dayOfWeekString = new DateFormatSymbols().getWeekdays();
750 
751         mMonthRepeatByDayOfWeekStrs = new String[7][];
752         // from Time.SUNDAY as 0 through Time.SATURDAY as 6
753         mMonthRepeatByDayOfWeekStrs[0] = mResources.getStringArray(R.array.repeat_by_nth_sun);
754         mMonthRepeatByDayOfWeekStrs[1] = mResources.getStringArray(R.array.repeat_by_nth_mon);
755         mMonthRepeatByDayOfWeekStrs[2] = mResources.getStringArray(R.array.repeat_by_nth_tues);
756         mMonthRepeatByDayOfWeekStrs[3] = mResources.getStringArray(R.array.repeat_by_nth_wed);
757         mMonthRepeatByDayOfWeekStrs[4] = mResources.getStringArray(R.array.repeat_by_nth_thurs);
758         mMonthRepeatByDayOfWeekStrs[5] = mResources.getStringArray(R.array.repeat_by_nth_fri);
759         mMonthRepeatByDayOfWeekStrs[6] = mResources.getStringArray(R.array.repeat_by_nth_sat);
760 
761         // In Time.java day of week order e.g. Sun = 0
762         int idx = Utils.getFirstDayOfWeek(getActivity());
763 
764         // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7
765         dayOfWeekString = new DateFormatSymbols().getShortWeekdays();
766 
767         int numOfButtonsInRow1;
768         int numOfButtonsInRow2;
769 
770         if (mResources.getConfiguration().screenWidthDp > MIN_SCREEN_WIDTH_FOR_SINGLE_ROW_WEEK) {
771             numOfButtonsInRow1 = 7;
772             numOfButtonsInRow2 = 0;
773             mWeekGroup2.setVisibility(View.GONE);
774             mWeekGroup2.getChildAt(3).setVisibility(View.GONE);
775         } else {
776             numOfButtonsInRow1 = 4;
777             numOfButtonsInRow2 = 3;
778 
779             mWeekGroup2.setVisibility(View.VISIBLE);
780             // Set rightmost button on the second row invisible so it takes up
781             // space and everything centers properly
782             mWeekGroup2.getChildAt(3).setVisibility(View.INVISIBLE);
783         }
784 
785         /* First row */
786         for (int i = 0; i < 7; i++) {
787             if (i >= numOfButtonsInRow1) {
788                 mWeekGroup.getChildAt(i).setVisibility(View.GONE);
789                 continue;
790             }
791 
792             mWeekByDayButtons[idx] = (ToggleButton) mWeekGroup.getChildAt(i);
793             mWeekByDayButtons[idx].setTextOff(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
794             mWeekByDayButtons[idx].setTextOn(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
795             mWeekByDayButtons[idx].setOnCheckedChangeListener(this);
796 
797             if (++idx >= 7) {
798                 idx = 0;
799             }
800         }
801 
802         /* 2nd Row */
803         for (int i = 0; i < 3; i++) {
804             if (i >= numOfButtonsInRow2) {
805                 mWeekGroup2.getChildAt(i).setVisibility(View.GONE);
806                 continue;
807             }
808             mWeekByDayButtons[idx] = (ToggleButton) mWeekGroup2.getChildAt(i);
809             mWeekByDayButtons[idx].setTextOff(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
810             mWeekByDayButtons[idx].setTextOn(dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[idx]]);
811             mWeekByDayButtons[idx].setOnCheckedChangeListener(this);
812 
813             if (++idx >= 7) {
814                 idx = 0;
815             }
816         }
817 
818         mMonthGroup = (LinearLayout) mView.findViewById(R.id.monthGroup);
819         mMonthRepeatByRadioGroup = (RadioGroup) mView.findViewById(R.id.monthGroup);
820         mMonthRepeatByRadioGroup.setOnCheckedChangeListener(this);
821         mRepeatMonthlyByNthDayOfWeek = (RadioButton) mView
822                 .findViewById(R.id.repeatMonthlyByNthDayOfTheWeek);
823         mRepeatMonthlyByNthDayOfMonth = (RadioButton) mView
824                 .findViewById(R.id.repeatMonthlyByNthDayOfMonth);
825 
826         mDone = (Button) mView.findViewById(R.id.done);
827         mDone.setOnClickListener(this);
828 
829         togglePickerOptions();
830         updateDialog();
831         if (endCountHasFocus) {
832             mEndCount.requestFocus();
833         }
834         return mView;
835     }
836 
togglePickerOptions()837     private void togglePickerOptions() {
838         if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) {
839             mFreqSpinner.setEnabled(false);
840             mEndSpinner.setEnabled(false);
841             mIntervalPreText.setEnabled(false);
842             mInterval.setEnabled(false);
843             mIntervalPostText.setEnabled(false);
844             mMonthRepeatByRadioGroup.setEnabled(false);
845             mEndCount.setEnabled(false);
846             mPostEndCount.setEnabled(false);
847             mEndDateTextView.setEnabled(false);
848             mRepeatMonthlyByNthDayOfWeek.setEnabled(false);
849             mRepeatMonthlyByNthDayOfMonth.setEnabled(false);
850             for (Button button : mWeekByDayButtons) {
851                 button.setEnabled(false);
852             }
853         } else {
854             mView.findViewById(R.id.options).setEnabled(true);
855             mFreqSpinner.setEnabled(true);
856             mEndSpinner.setEnabled(true);
857             mIntervalPreText.setEnabled(true);
858             mInterval.setEnabled(true);
859             mIntervalPostText.setEnabled(true);
860             mMonthRepeatByRadioGroup.setEnabled(true);
861             mEndCount.setEnabled(true);
862             mPostEndCount.setEnabled(true);
863             mEndDateTextView.setEnabled(true);
864             mRepeatMonthlyByNthDayOfWeek.setEnabled(true);
865             mRepeatMonthlyByNthDayOfMonth.setEnabled(true);
866             for (Button button : mWeekByDayButtons) {
867                 button.setEnabled(true);
868             }
869         }
870         updateDoneButtonState();
871     }
872 
updateDoneButtonState()873     private void updateDoneButtonState() {
874         if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) {
875             mDone.setEnabled(true);
876             return;
877         }
878 
879         if (mInterval.getText().toString().length() == 0) {
880             mDone.setEnabled(false);
881             return;
882         }
883 
884         if (mEndCount.getVisibility() == View.VISIBLE &&
885                 mEndCount.getText().toString().length() == 0) {
886             mDone.setEnabled(false);
887             return;
888         }
889 
890         if (mModel.freq == RecurrenceModel.FREQ_WEEKLY) {
891             for (CompoundButton b : mWeekByDayButtons) {
892                 if (b.isChecked()) {
893                     mDone.setEnabled(true);
894                     return;
895                 }
896             }
897             mDone.setEnabled(false);
898             return;
899         }
900 
901         mDone.setEnabled(true);
902     }
903 
904     @Override
onSaveInstanceState(Bundle outState)905     public void onSaveInstanceState(Bundle outState) {
906         super.onSaveInstanceState(outState);
907         outState.putParcelable(BUNDLE_MODEL, mModel);
908         if (mEndCount.hasFocus()) {
909             outState.putBoolean(BUNDLE_END_COUNT_HAS_FOCUS, true);
910         }
911     }
912 
updateDialog()913     public void updateDialog() {
914         // Interval
915         // Checking before setting because this causes infinite recursion
916         // in afterTextWatcher
917         final String intervalStr = Integer.toString(mModel.interval);
918         if (!intervalStr.equals(mInterval.getText().toString())) {
919             mInterval.setText(intervalStr);
920         }
921 
922         mFreqSpinner.setSelection(mModel.freq);
923         mWeekGroup.setVisibility(mModel.freq == RecurrenceModel.FREQ_WEEKLY ? View.VISIBLE : View.GONE);
924         mWeekGroup2.setVisibility(mModel.freq == RecurrenceModel.FREQ_WEEKLY ? View.VISIBLE : View.GONE);
925         mMonthGroup.setVisibility(mModel.freq == RecurrenceModel.FREQ_MONTHLY ? View.VISIBLE : View.GONE);
926 
927         switch (mModel.freq) {
928             case RecurrenceModel.FREQ_DAILY:
929                 mIntervalResId = R.plurals.recurrence_interval_daily;
930                 break;
931 
932             case RecurrenceModel.FREQ_WEEKLY:
933                 mIntervalResId = R.plurals.recurrence_interval_weekly;
934                 for (int i = 0; i < 7; i++) {
935                     mWeekByDayButtons[i].setChecked(mModel.weeklyByDayOfWeek[i]);
936                 }
937                 break;
938 
939             case RecurrenceModel.FREQ_MONTHLY:
940                 mIntervalResId = R.plurals.recurrence_interval_monthly;
941 
942                 if (mModel.monthlyRepeat == RecurrenceModel.MONTHLY_BY_DATE) {
943                     mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfMonth);
944                 } else if (mModel.monthlyRepeat == RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK) {
945                     mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfTheWeek);
946                 }
947 
948                 if (mMonthRepeatByDayOfWeekStr == null) {
949                     if (mModel.monthlyByNthDayOfWeek == 0) {
950                         mModel.monthlyByNthDayOfWeek = (mTime.monthDay + 6) / 7;
951                         // Since not all months have 5 weeks, we convert 5th NthDayOfWeek to
952                         // -1 for last monthly day of the week
953                         if (mModel.monthlyByNthDayOfWeek >= FIFTH_WEEK_IN_A_MONTH) {
954                             mModel.monthlyByNthDayOfWeek = LAST_NTH_DAY_OF_WEEK;
955                         }
956                         mModel.monthlyByDayOfWeek = mTime.weekDay;
957                     }
958 
959                     String[] monthlyByNthDayOfWeekStrs =
960                             mMonthRepeatByDayOfWeekStrs[mModel.monthlyByDayOfWeek];
961 
962                     // TODO(psliwowski): Find a better way handle -1 indexes
963                     int msgIndex = mModel.monthlyByNthDayOfWeek < 0 ? FIFTH_WEEK_IN_A_MONTH :
964                             mModel.monthlyByNthDayOfWeek;
965                     mMonthRepeatByDayOfWeekStr =
966                             monthlyByNthDayOfWeekStrs[msgIndex - 1];
967                     mRepeatMonthlyByNthDayOfWeek.setText(mMonthRepeatByDayOfWeekStr);
968                 }
969                 break;
970 
971             case RecurrenceModel.FREQ_YEARLY:
972                 mIntervalResId = R.plurals.recurrence_interval_yearly;
973                 break;
974         }
975         updateIntervalText();
976         updateDoneButtonState();
977 
978         mEndSpinner.setSelection(mModel.end);
979         if (mModel.end == RecurrenceModel.END_BY_DATE) {
980             final String dateStr = DateUtils.formatDateTime(getActivity(),
981                     mModel.endDate.toMillis(false), DateUtils.FORMAT_NUMERIC_DATE);
982             mEndDateTextView.setText(dateStr);
983         } else {
984             if (mModel.end == RecurrenceModel.END_BY_COUNT) {
985                 // Checking before setting because this causes infinite
986                 // recursion
987                 // in afterTextWatcher
988                 final String countStr = Integer.toString(mModel.endCount);
989                 if (!countStr.equals(mEndCount.getText().toString())) {
990                     mEndCount.setText(countStr);
991                 }
992             }
993         }
994     }
995 
996     /**
997      * @param endDateString
998      */
999     private void setEndSpinnerEndDateStr(final String endDateString) {
1000         mEndSpinnerArray.set(1, endDateString);
1001         mEndSpinnerAdapter.notifyDataSetChanged();
1002     }
1003 
1004     private void doToast() {
1005         Log.e(TAG, "Model = " + mModel.toString());
1006         String rrule;
1007         if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) {
1008             rrule = "Not repeating";
1009         } else {
1010             copyModelToEventRecurrence(mModel, mRecurrence);
1011             rrule = mRecurrence.toString();
1012         }
1013 
1014         if (mToast != null) {
1015             mToast.cancel();
1016         }
1017         mToast = Toast.makeText(getActivity(), rrule,
1018                 Toast.LENGTH_LONG);
1019         mToast.show();
1020     }
1021 
1022     // TODO Test and update for Right-to-Left
1023     private void updateIntervalText() {
1024         if (mIntervalResId == -1) {
1025             return;
1026         }
1027 
1028         final String INTERVAL_COUNT_MARKER = "%d";
1029         String intervalString = mResources.getQuantityString(mIntervalResId, mModel.interval);
1030         int markerStart = intervalString.indexOf(INTERVAL_COUNT_MARKER);
1031 
1032         if (markerStart != -1) {
1033           int postTextStart = markerStart + INTERVAL_COUNT_MARKER.length();
1034           mIntervalPostText.setText(intervalString.substring(postTextStart,
1035                   intervalString.length()).trim());
1036           mIntervalPreText.setText(intervalString.substring(0, markerStart).trim());
1037         }
1038     }
1039 
1040     /**
1041      * Update the "Repeat for N events" end option with the proper string values
1042      * based on the value that has been entered for N.
1043      */
1044     private void updateEndCountText() {
1045         final String END_COUNT_MARKER = "%d";
1046         String endString = mResources.getQuantityString(R.plurals.recurrence_end_count,
1047                 mModel.endCount);
1048         int markerStart = endString.indexOf(END_COUNT_MARKER);
1049 
1050         if (markerStart != -1) {
1051             if (markerStart == 0) {
1052                 Log.e(TAG, "No text to put in to recurrence's end spinner.");
1053             } else {
1054                 int postTextStart = markerStart + END_COUNT_MARKER.length();
1055                 mPostEndCount.setText(endString.substring(postTextStart,
1056                         endString.length()).trim());
1057             }
1058         }
1059     }
1060 
1061     // Implements OnItemSelectedListener interface
1062     // Freq spinner
1063     // End spinner
1064     @Override
1065     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1066         if (parent == mFreqSpinner) {
1067             mModel.freq = position;
1068         } else if (parent == mEndSpinner) {
1069             switch (position) {
1070                 case RecurrenceModel.END_NEVER:
1071                     mModel.end = RecurrenceModel.END_NEVER;
1072                     break;
1073                 case RecurrenceModel.END_BY_DATE:
1074                     mModel.end = RecurrenceModel.END_BY_DATE;
1075                     break;
1076                 case RecurrenceModel.END_BY_COUNT:
1077                     mModel.end = RecurrenceModel.END_BY_COUNT;
1078 
1079                     if (mModel.endCount <= 1) {
1080                         mModel.endCount = 1;
1081                     } else if (mModel.endCount > COUNT_MAX) {
1082                         mModel.endCount = COUNT_MAX;
1083                     }
1084                     updateEndCountText();
1085                     break;
1086             }
1087             mEndCount.setVisibility(mModel.end == RecurrenceModel.END_BY_COUNT ? View.VISIBLE
1088                     : View.GONE);
1089             mEndDateTextView.setVisibility(mModel.end == RecurrenceModel.END_BY_DATE ? View.VISIBLE
1090                     : View.GONE);
1091             mPostEndCount.setVisibility(
1092                     mModel.end == RecurrenceModel.END_BY_COUNT  && !mHidePostEndCount?
1093                             View.VISIBLE : View.GONE);
1094 
1095         }
1096         updateDialog();
1097     }
1098 
1099     // Implements OnItemSelectedListener interface
1100     @Override
1101     public void onNothingSelected(AdapterView<?> arg0) {
1102     }
1103 
1104     @Override
1105     public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) {
1106         if (mModel.endDate == null) {
1107             mModel.endDate = new Time(mTime.timezone);
1108             mModel.endDate.hour = mModel.endDate.minute = mModel.endDate.second = 0;
1109         }
1110         mModel.endDate.year = year;
1111         mModel.endDate.month = monthOfYear;
1112         mModel.endDate.monthDay = dayOfMonth;
1113         mModel.endDate.normalize(false);
1114         updateDialog();
1115     }
1116 
1117     // Implements OnCheckedChangeListener interface
1118     // Week repeat by day of week
1119     @Override
1120     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
1121         int itemIdx = -1;
1122         for (int i = 0; i < 7; i++) {
1123             if (itemIdx == -1 && buttonView == mWeekByDayButtons[i]) {
1124                 itemIdx = i;
1125                 mModel.weeklyByDayOfWeek[i] = isChecked;
1126             }
1127         }
1128         updateDialog();
1129     }
1130 
1131     // Implements android.widget.RadioGroup.OnCheckedChangeListener interface
1132     // Month repeat by radio buttons
1133     @Override
1134     public void onCheckedChanged(RadioGroup group, int checkedId) {
1135         if (checkedId == R.id.repeatMonthlyByNthDayOfMonth) {
1136             mModel.monthlyRepeat = RecurrenceModel.MONTHLY_BY_DATE;
1137         } else if (checkedId == R.id.repeatMonthlyByNthDayOfTheWeek) {
1138             mModel.monthlyRepeat = RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK;
1139         }
1140         updateDialog();
1141     }
1142 
1143     // Implements OnClickListener interface
1144     // EndDate button
1145     // Done button
1146     @Override
1147     public void onClick(View v) {
1148         if (mEndDateTextView == v) {
1149             if (mDatePickerDialog != null) {
1150                 mDatePickerDialog.dismiss();
1151             }
1152             mDatePickerDialog = DatePickerDialog.newInstance(this, mModel.endDate.year,
1153                     mModel.endDate.month, mModel.endDate.monthDay);
1154             mDatePickerDialog.setFirstDayOfWeek(Utils.getFirstDayOfWeekAsCalendar(getActivity()));
1155             mDatePickerDialog.setYearRange(Utils.YEAR_MIN, Utils.YEAR_MAX);
1156             mDatePickerDialog.show(getFragmentManager(), FRAG_TAG_DATE_PICKER);
1157         } else if (mDone == v) {
1158             String rrule;
1159             if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) {
1160                 rrule = null;
1161             } else {
1162                 copyModelToEventRecurrence(mModel, mRecurrence);
1163                 rrule = mRecurrence.toString();
1164             }
1165             mRecurrenceSetListener.onRecurrenceSet(rrule);
1166             dismiss();
1167         }
1168     }
1169 
1170     @Override
1171     public void onActivityCreated(Bundle savedInstanceState) {
1172         super.onActivityCreated(savedInstanceState);
1173         mDatePickerDialog = (DatePickerDialog) getFragmentManager()
1174                 .findFragmentByTag(FRAG_TAG_DATE_PICKER);
1175         if (mDatePickerDialog != null) {
1176             mDatePickerDialog.setOnDateSetListener(this);
1177         }
1178     }
1179 
1180     public interface OnRecurrenceSetListener {
1181         void onRecurrenceSet(String rrule);
1182     }
1183 
1184     public void setOnRecurrenceSetListener(OnRecurrenceSetListener l) {
1185         mRecurrenceSetListener = l;
1186     }
1187 
1188     private class EndSpinnerAdapter extends ArrayAdapter<CharSequence> {
1189         final String END_DATE_MARKER = "%s";
1190         final String END_COUNT_MARKER = "%d";
1191 
1192         private LayoutInflater mInflater;
1193         private int mItemResourceId;
1194         private int mTextResourceId;
1195         private ArrayList<CharSequence> mStrings;
1196         private String mEndDateString;
1197         private boolean mUseFormStrings;
1198 
1199         /**
1200          * @param context
1201          * @param textViewResourceId
1202          * @param objects
1203          */
1204         public EndSpinnerAdapter(Context context, ArrayList<CharSequence> strings,
1205                 int itemResourceId, int textResourceId) {
1206             super(context, itemResourceId, strings);
1207             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1208             mItemResourceId = itemResourceId;
1209             mTextResourceId = textResourceId;
1210             mStrings = strings;
1211             mEndDateString = getResources().getString(R.string.recurrence_end_date);
1212 
1213             // If either date or count strings don't translate well, such that we aren't assured
1214             // to have some text available to be placed in the spinner, then we'll have to use
1215             // the more form-like versions of both strings instead.
1216             int markerStart = mEndDateString.indexOf(END_DATE_MARKER);
1217             if (markerStart <= 0) {
1218                 // The date string does not have any text before the "%s" so we'll have to use the
1219                 // more form-like strings instead.
1220                 mUseFormStrings = true;
1221             } else {
1222                 String countEndStr = getResources().getQuantityString(
1223                         R.plurals.recurrence_end_count, 1);
1224                 markerStart = countEndStr.indexOf(END_COUNT_MARKER);
1225                 if (markerStart <= 0) {
1226                     // The count string does not have any text before the "%d" so we'll have to use
1227                     // the more form-like strings instead.
1228                     mUseFormStrings = true;
1229                 }
1230             }
1231 
1232             if (mUseFormStrings) {
1233                 // We'll have to set the layout for the spinner to be weight=0 so it doesn't
1234                 // take up too much space.
1235                 mEndSpinner.setLayoutParams(
1236                         new TableLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f));
1237             }
1238         }
1239 
1240         @Override
1241         public View getView(int position, View convertView, ViewGroup parent) {
1242             View v;
1243             // Check if we can recycle the view
1244             if (convertView == null) {
1245                 v = mInflater.inflate(mTextResourceId, parent, false);
1246             } else {
1247                 v = convertView;
1248             }
1249 
1250             TextView item = (TextView) v.findViewById(R.id.spinner_item);
1251             int markerStart;
1252             switch (position) {
1253                 case RecurrenceModel.END_NEVER:
1254                     item.setText(mStrings.get(RecurrenceModel.END_NEVER));
1255                     break;
1256                 case RecurrenceModel.END_BY_DATE:
1257                     markerStart = mEndDateString.indexOf(END_DATE_MARKER);
1258 
1259                     if (markerStart != -1) {
1260                         if (mUseFormStrings || markerStart == 0) {
1261                             // If we get here, the translation of "Until" doesn't work correctly,
1262                             // so we'll just set the whole "Until a date" string.
1263                             item.setText(mEndDateLabel);
1264                         } else {
1265                             item.setText(mEndDateString.substring(0, markerStart).trim());
1266                         }
1267                     }
1268                     break;
1269                 case RecurrenceModel.END_BY_COUNT:
1270                     String endString = mResources.getQuantityString(R.plurals.recurrence_end_count,
1271                             mModel.endCount);
1272                     markerStart = endString.indexOf(END_COUNT_MARKER);
1273 
1274                     if (markerStart != -1) {
1275                         if (mUseFormStrings || markerStart == 0) {
1276                             // If we get here, the translation of "For" doesn't work correctly,
1277                             // so we'll just set the whole "For a number of events" string.
1278                             item.setText(mEndCountLabel);
1279                             // Also, we'll hide the " events" that would have been at the end.
1280                             mPostEndCount.setVisibility(View.GONE);
1281                             // Use this flag so the onItemSelected knows whether to show it later.
1282                             mHidePostEndCount = true;
1283                         } else {
1284                             int postTextStart = markerStart + END_COUNT_MARKER.length();
1285                             mPostEndCount.setText(endString.substring(postTextStart,
1286                                     endString.length()).trim());
1287                             // In case it's a recycled view that wasn't visible.
1288                             if (mModel.end == RecurrenceModel.END_BY_COUNT) {
1289                                 mPostEndCount.setVisibility(View.VISIBLE);
1290                             }
1291                             if (endString.charAt(markerStart - 1) == ' ') {
1292                                 markerStart--;
1293                             }
1294                             item.setText(endString.substring(0, markerStart).trim());
1295                         }
1296                     }
1297                     break;
1298                 default:
1299                     v = null;
1300                     break;
1301             }
1302 
1303             return v;
1304         }
1305 
1306         @Override
1307         public View getDropDownView(int position, View convertView, ViewGroup parent) {
1308             View v;
1309             // Check if we can recycle the view
1310             if (convertView == null) {
1311                 v = mInflater.inflate(mItemResourceId, parent, false);
1312             } else {
1313                 v = convertView;
1314             }
1315 
1316             TextView item = (TextView) v.findViewById(R.id.spinner_item);
1317             item.setText(mStrings.get(position));
1318 
1319             return v;
1320         }
1321     }
1322 }
1323