Java tutorial
/* * Copyright (C) 2013 The Android Open Source Project * Copyright 2015 Vikram Kakkar * * 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.appeaser.sublimepickerlibrary.recurrencepicker; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; import android.util.TimeFormatException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CompoundButton; import android.widget.DatePicker; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; import com.appeaser.sublimepickerlibrary.R; import com.appeaser.sublimepickerlibrary.common.DecisionButtonLayout; import com.appeaser.sublimepickerlibrary.datepicker.RecurrenceEndDatePicker; import com.appeaser.sublimepickerlibrary.drawables.CheckableDrawable; import com.appeaser.sublimepickerlibrary.utilities.RecurrenceUtils; import com.appeaser.sublimepickerlibrary.utilities.SUtils; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; /** * Helps create a custom recurrence-rule. */ public class RecurrenceOptionCreator extends FrameLayout implements AdapterView.OnItemSelectedListener, RadioGroup.OnCheckedChangeListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener, RecurrenceEndDatePicker.OnDateSetListener { private static final String TAG = "RecurrenceOptionCreator"; // Used to keep track of currently visible view - the view that // will be restored on screen rotation. private enum CurrentView { RECURRENCE_PICKER, DATE_ONLY_PICKER } // 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; // Special cases in monthlyByNthDayOfWeek private static final int FIFTH_WEEK_IN_A_MONTH = 5; private static final int LAST_NTH_DAY_OF_WEEK = -1; // Stripped down version of 'SublimeMaterialDatePicker' //private DatePickerView mDateOnlyPicker; private RecurrenceEndDatePicker mDateOnlyPicker; private View mRecurrencePicker; // OK/Cancel buttons private DecisionButtonLayout mButtonLayout; // Uses either to DateFormat.SHORT or DateFormat.MEDIUM // to format the supplied end date. The option can only be // set in RecurrenceOptionCreator's style-definition private DateFormat mEndDateFormatter; private Resources mResources; private EventRecurrence mRecurrence = new EventRecurrence(); private Time mTime = new Time(); // TODO timezone? private RecurrenceModel mModel = new RecurrenceModel(); 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 Spinner mFreqSpinner; private static final int[] mFreqModelToEventRecurrence = { EventRecurrence.DAILY, EventRecurrence.WEEKLY, EventRecurrence.MONTHLY, EventRecurrence.YEARLY }; private EditText mInterval; private TextView mIntervalPreText; private TextView mIntervalPostText; private int mIntervalResId = -1; private Spinner mEndSpinner; private TextView mEndDateTextView; private EditText mEndCount; private TextView mPostEndCount; private boolean mHidePostEndCount; private ArrayList<CharSequence> mEndSpinnerArray = new ArrayList<>(3); private EndSpinnerAdapter mEndSpinnerAdapter; private String mEndNeverStr; private String mEndDateLabel; private String mEndCountLabel; /** * Hold toggle buttons in the order per user's first day of week preference */ private LinearLayout mWeekGroup; private LinearLayout mWeekGroup2; // Sun = 0 private WeekButton[] mWeekByDayButtons = new WeekButton[7]; /** * A double array of Strings to hold the 7x5 list of possible strings of the form: * "on every [Nth] [DAY_OF_WEEK]", e.g. "on every second Monday", * where [Nth] can be [first, second, third, fourth, last] */ private String[][] mMonthRepeatByDayOfWeekStrs; private RadioGroup mMonthRepeatByRadioGroup; private RadioButton mRepeatMonthlyByNthDayOfWeek; private RadioButton mRepeatMonthlyByNthDayOfMonth; private String mMonthRepeatByDayOfWeekStr; private OnRecurrenceSetListener mRecurrenceSetListener; int mHeaderBackgroundColor; private DecisionButtonLayout.Callback mButtonLayoutCallback = new DecisionButtonLayout.Callback() { @Override public void onOkay() { String rrule; if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) { rrule = null; } else { copyModelToEventRecurrence(mModel, mRecurrence); rrule = mRecurrence.toString(); } mRecurrenceSetListener.onRecurrenceSet(rrule); } @Override public void onCancel() { mRecurrenceSetListener.onCancelled(); } }; // { WIP: Provide a synonym for chosen custom option. } // // Eg: if 'freq' is 'WEEKLY' and all seven days of the week // are selected, the chosen option is equivalent to 'Repeats Daily' // Actual Recurrence Rule string is 'Repeats Weekly every // SUN, MON, TUE, WED, THU, FRI, SAT'. More options are possible. // // Another possible extension is - if 'freq' is 'YEARLY' and // 'interval' is set to 1, the custom option 'EVERY YEAR' is // already present in the 'SublimeRecurrencePicker' menu. Use // that instead of showing 'REPEATS YEARLY' at the top. @SuppressWarnings("unused") SublimeRecurrencePicker.RecurrenceOption resolveRepeatOption() { if (mModel.freq == RecurrenceModel.FREQ_DAILY) { if (mModel.interval == INTERVAL_DEFAULT && mModel.end == RecurrenceModel.END_NEVER) { return SublimeRecurrencePicker.RecurrenceOption.DAILY; } } /*else if (mModel.freq == RecurrenceModel.FREQ_WEEKLY) { }*/ return SublimeRecurrencePicker.RecurrenceOption.CUSTOM; } private class RecurrenceModel implements Parcelable { // 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; static final int STATE_NO_RECURRENCE = 0; static final int STATE_RECURRENCE = 1; int recurrenceState; /** * FREQ: Repeat pattern */ int freq = FREQ_WEEKLY; /** * INTERVAL: Every n days/weeks/months/years. n >= 1 */ int interval = INTERVAL_DEFAULT; /** * UNTIL and COUNT: How does the the event end? */ 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. */ 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=Last, 1=1st, 2=2nd, ..., 5=5th * <p/> * We support 5th, just to handle backwards capabilities with old bug, but it * gets converted to -1 once edited. */ 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 RecurrenceModel() { } public RecurrenceModel(Parcel in) { readFromParcel(in); } @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); dest.writeInt(recurrenceState); } private void readFromParcel(Parcel in) { freq = in.readInt(); interval = in.readInt(); end = in.readInt(); endDate = new Time(); 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(); recurrenceState = in.readInt(); } @SuppressWarnings("all") // suppress unused and hiding public final Parcelable.Creator<RecurrenceModel> CREATOR = new Creator<RecurrenceModel>() { public RecurrenceModel createFromParcel(Parcel in) { return new RecurrenceModel(in); } public RecurrenceModel[] newArray(int size) { return new RecurrenceModel[size]; } }; } 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)); } updateDoneButtonState(); 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) { } } static public boolean isSupportedMonthlyByNthDayOfWeek(int num) { // We only support monthlyByNthDayOfWeek when it is greater then 0 but less then 5. // Or if -1 when it is the last monthly day of the week. return (num > 0 && num <= FIFTH_WEEK_IN_A_MONTH) || num == LAST_NTH_DAY_OF_WEEK; } 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 (isSupportedMonthlyByNthDayOfWeek(er.bydayNum[i])) { ++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 don't lose data when getting data that our UI can't handle static private void copyEventRecurrenceToModel(final EventRecurrence er, RecurrenceModel model) { // Freq: switch (er.freq) { case EventRecurrence.DAILY: model.freq = RecurrenceModel.FREQ_DAILY; break; case EventRecurrence.MONTHLY: model.freq = RecurrenceModel.FREQ_MONTHLY; break; case EventRecurrence.YEARLY: model.freq = RecurrenceModel.FREQ_YEARLY; break; case EventRecurrence.WEEKLY: model.freq = RecurrenceModel.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 = RecurrenceModel.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 == RecurrenceModel.END_BY_COUNT && model.endDate != null) { throw new IllegalStateException("freq=" + er.freq); } model.end = RecurrenceModel.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 == RecurrenceModel.FREQ_MONTHLY && isSupportedMonthlyByNthDayOfWeek(er.bydayNum[i])) { // LIMITATION: Can handle only (one) weekDayNum in nth or last and only // when // monthly model.monthlyByDayOfWeek = dayOfWeek; model.monthlyByNthDayOfWeek = er.bydayNum[i]; model.monthlyRepeat = RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK; count++; } } if (model.freq == RecurrenceModel.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 == RecurrenceModel.FREQ_MONTHLY) { if (er.bymonthdayCount == 1) { if (model.monthlyRepeat == RecurrenceModel.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 = RecurrenceModel.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 RecurrenceModel model, EventRecurrence er) { if (model.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) { 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 RecurrenceModel.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 RecurrenceModel.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 RecurrenceModel.FREQ_MONTHLY: if (model.monthlyRepeat == RecurrenceModel.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 == RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK) { if (!isSupportedMonthlyByNthDayOfWeek(model.monthlyByNthDayOfWeek)) { 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 RecurrenceModel.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()); } } public RecurrenceOptionCreator(Context context) { this(context, null); } public RecurrenceOptionCreator(Context context, AttributeSet attrs) { this(context, attrs, R.attr.spRecurrenceOptionCreatorStyle); } public RecurrenceOptionCreator(Context context, AttributeSet attrs, int defStyleAttr) { super(SUtils.createThemeWrapper(context, R.attr.sublimePickerStyle, R.style.SublimePickerStyleLight, R.attr.spRecurrenceOptionCreatorStyle, R.style.RecurrenceOptionCreatorStyle), attrs, defStyleAttr); initializeLayout(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public RecurrenceOptionCreator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(SUtils.createThemeWrapper(context, R.attr.sublimePickerStyle, R.style.SublimePickerStyleLight, R.attr.spRecurrenceOptionCreatorStyle, R.style.RecurrenceOptionCreatorStyle), attrs, defStyleAttr, defStyleRes); initializeLayout(); } // Initialize UI void initializeLayout() { int weekButtonUnselectedTextColor, weekButtonSelectedTextColor, weekButtonSelectedCircleColor; final TypedArray a = getContext().obtainStyledAttributes(R.styleable.RecurrenceOptionCreator); try { mHeaderBackgroundColor = a.getColor(R.styleable.RecurrenceOptionCreator_spHeaderBackground, 0); int endDateFormat = a.getInt(R.styleable.RecurrenceOptionCreator_spEndDateFormat, 1); mEndDateFormatter = DateFormat.getDateInstance( endDateFormat == 0 ? DateFormat.SHORT : DateFormat.MEDIUM, Locale.getDefault()); weekButtonUnselectedTextColor = a.getColor( R.styleable.RecurrenceOptionCreator_spWeekButtonUnselectedTextColor, SUtils.COLOR_ACCENT); weekButtonSelectedTextColor = a.getColor( R.styleable.RecurrenceOptionCreator_spWeekButtonSelectedTextColor, SUtils.COLOR_TEXT_PRIMARY_INVERSE); weekButtonSelectedCircleColor = a.getColor( R.styleable.RecurrenceOptionCreator_spWeekButtonSelectedCircleColor, SUtils.COLOR_ACCENT); } finally { a.recycle(); } mResources = getResources(); LayoutInflater.from(getContext()).inflate(R.layout.recurrence_picker, this); mRecurrencePicker = findViewById(R.id.recurrence_picker); mDateOnlyPicker = (RecurrenceEndDatePicker) findViewById(R.id.date_only_picker); mDateOnlyPicker.setVisibility(View.GONE); // OK/Cancel buttons mButtonLayout = (DecisionButtonLayout) findViewById(R.id.roc_decision_button_layout); mButtonLayout.applyOptions(mButtonLayoutCallback); SUtils.setViewBackground(findViewById(R.id.freqSpinnerHolder), mHeaderBackgroundColor, SUtils.CORNER_TOP_LEFT | SUtils.CORNER_TOP_RIGHT); /** EFrequency Spinner {Repeat daily, Repeat weekly, Repeat monthly, Repeat yearly} **/ mFreqSpinner = (Spinner) findViewById(R.id.freqSpinner); mFreqSpinner.setOnItemSelectedListener(this); ArrayAdapter<CharSequence> freqAdapter = ArrayAdapter.createFromResource(getContext(), R.array.recurrence_freq, R.layout.roc_freq_spinner_item); freqAdapter.setDropDownViewResource(R.layout.roc_spinner_dropdown_item); mFreqSpinner.setAdapter(freqAdapter); Drawable freqSpinnerBg = ContextCompat.getDrawable(getContext(), R.drawable.abc_spinner_mtrl_am_alpha); PorterDuffColorFilter cfFreqSpinner = new PorterDuffColorFilter(SUtils.COLOR_TEXT_PRIMARY_INVERSE, PorterDuff.Mode.SRC_IN); if (freqSpinnerBg != null) { freqSpinnerBg.setColorFilter(cfFreqSpinner); SUtils.setViewBackground(mFreqSpinner, freqSpinnerBg); } mInterval = (EditText) findViewById(R.id.interval); mInterval.addTextChangedListener(new minMaxTextWatcher(1, INTERVAL_DEFAULT, INTERVAL_MAX) { @Override void onChange(int v) { if (mIntervalResId != -1 && mInterval.getText().toString().length() > 0) { mModel.interval = v; updateIntervalText(); mInterval.requestLayout(); } } }); mIntervalPreText = (TextView) findViewById(R.id.intervalPreText); mIntervalPostText = (TextView) findViewById(R.id.intervalPostText); /** End Spinner {Forever, Until a date, For a number of events} **/ mEndNeverStr = mResources.getString(R.string.recurrence_end_continously); mEndDateLabel = mResources.getString(R.string.recurrence_end_date_label); mEndCountLabel = mResources.getString(R.string.recurrence_end_count_label); mEndSpinnerArray.add(mEndNeverStr); mEndSpinnerArray.add(mEndDateLabel); mEndSpinnerArray.add(mEndCountLabel); mEndSpinner = (Spinner) findViewById(R.id.endSpinner); mEndSpinner.setOnItemSelectedListener(this); mEndSpinnerAdapter = new EndSpinnerAdapter(getContext(), mEndSpinnerArray, R.layout.roc_end_spinner_item, R.id.spinner_item, R.layout.roc_spinner_dropdown_item); mEndSpinner.setAdapter(mEndSpinnerAdapter); mEndCount = (EditText) findViewById(R.id.endCount); mEndCount.addTextChangedListener(new minMaxTextWatcher(1, COUNT_DEFAULT, COUNT_MAX) { @Override void onChange(int v) { if (mModel.endCount != v) { mModel.endCount = v; updateEndCountText(); mEndCount.requestLayout(); } } }); mPostEndCount = (TextView) findViewById(R.id.postEndCount); mEndDateTextView = (TextView) findViewById(R.id.endDate); mEndDateTextView.setOnClickListener(this); SUtils.setViewBackground(mEndDateTextView, SUtils.createButtonBg(getContext(), SUtils.COLOR_BUTTON_NORMAL, SUtils.COLOR_CONTROL_HIGHLIGHT)); // set default & checked state colors WeekButton.setStateColors(weekButtonUnselectedTextColor, weekButtonSelectedTextColor); // AOSP code handled this differently. It has been refactored to // let Android decide if we have enough space to show // all seven 'WeekButtons' inline. In this case, 'mWeekGroup2' // will be null (see @layout-w460dp/week_buttons). mWeekGroup = (LinearLayout) findViewById(R.id.weekGroup); mWeekGroup2 = (LinearLayout) findViewById(R.id.weekGroup2); // Only non-null when available width is < 460dp // Used only for positioning 'WeekButtons' in two rows // of 4 & 3. View eighthWeekDay = findViewById(R.id.week_day_8); if (eighthWeekDay != null) eighthWeekDay.setVisibility(View.INVISIBLE); // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7 //String[] dayOfWeekString = new DateFormatSymbols().getWeekdays(); mMonthRepeatByDayOfWeekStrs = new String[7][]; // from Time.SUNDAY as 0 through Time.SATURDAY as 6 mMonthRepeatByDayOfWeekStrs[0] = mResources.getStringArray(R.array.repeat_by_nth_sun); mMonthRepeatByDayOfWeekStrs[1] = mResources.getStringArray(R.array.repeat_by_nth_mon); mMonthRepeatByDayOfWeekStrs[2] = mResources.getStringArray(R.array.repeat_by_nth_tues); mMonthRepeatByDayOfWeekStrs[3] = mResources.getStringArray(R.array.repeat_by_nth_wed); mMonthRepeatByDayOfWeekStrs[4] = mResources.getStringArray(R.array.repeat_by_nth_thurs); mMonthRepeatByDayOfWeekStrs[5] = mResources.getStringArray(R.array.repeat_by_nth_fri); mMonthRepeatByDayOfWeekStrs[6] = mResources.getStringArray(R.array.repeat_by_nth_sat); // In Time.java day of week order e.g. Sun = 0 int idx = RecurrenceUtils.getFirstDayOfWeek(); // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7 String[] dayOfWeekString = new DateFormatSymbols().getShortWeekdays(); // CheckableDrawable's width & height int expandedWidthHeight = mResources.getDimensionPixelSize(R.dimen.week_button_state_on_circle_size); WeekButton[] tempWeekButtons = new WeekButton[7]; tempWeekButtons[0] = (WeekButton) findViewById(R.id.week_day_1); tempWeekButtons[1] = (WeekButton) findViewById(R.id.week_day_2); tempWeekButtons[2] = (WeekButton) findViewById(R.id.week_day_3); tempWeekButtons[3] = (WeekButton) findViewById(R.id.week_day_4); tempWeekButtons[4] = (WeekButton) findViewById(R.id.week_day_5); tempWeekButtons[5] = (WeekButton) findViewById(R.id.week_day_6); tempWeekButtons[6] = (WeekButton) findViewById(R.id.week_day_7); for (int i = 0; i < mWeekByDayButtons.length; i++) { mWeekByDayButtons[idx] = tempWeekButtons[i]; SUtils.setViewBackground(mWeekByDayButtons[idx], new CheckableDrawable(weekButtonSelectedCircleColor, false, expandedWidthHeight)); mWeekByDayButtons[idx].setTextColor(weekButtonUnselectedTextColor); 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; } } mMonthRepeatByRadioGroup = (RadioGroup) findViewById(R.id.monthGroup); mMonthRepeatByRadioGroup.setOnCheckedChangeListener(this); mRepeatMonthlyByNthDayOfWeek = (RadioButton) findViewById(R.id.repeatMonthlyByNthDayOfTheWeek); mRepeatMonthlyByNthDayOfMonth = (RadioButton) findViewById(R.id.repeatMonthlyByNthDayOfMonth); } public void initializeData(long currentlyChosenTime, String timeZone, String recurrenceRule, @NonNull OnRecurrenceSetListener callback) { mRecurrence.wkst = EventRecurrence.timeDay2Day(RecurrenceUtils.getFirstDayOfWeek()); mRecurrenceSetListener = callback; mTime.set(currentlyChosenTime); if (!TextUtils.isEmpty(timeZone)) { mTime.timezone = timeZone; } mTime.normalize(false); // Time days of week: Sun=0, Mon=1, etc mModel.weeklyByDayOfWeek[mTime.weekDay] = true; if (!TextUtils.isEmpty(recurrenceRule)) { mModel.recurrenceState = RecurrenceModel.STATE_RECURRENCE; mRecurrence.parse(recurrenceRule); copyEventRecurrenceToModel(mRecurrence, mModel); // Leave today's day of week as checked by default in weekly view. if (mRecurrence.bydayCount == 0) { mModel.weeklyByDayOfWeek[mTime.weekDay] = true; } } else { // Default mModel.recurrenceState = RecurrenceModel.STATE_RECURRENCE; } if (mModel.endDate == null) { mModel.endDate = new Time(mTime); switch (mModel.freq) { case RecurrenceModel.FREQ_DAILY: case RecurrenceModel.FREQ_WEEKLY: mModel.endDate.month += 1; break; case RecurrenceModel.FREQ_MONTHLY: mModel.endDate.month += 3; break; case RecurrenceModel.FREQ_YEARLY: mModel.endDate.year += 3; break; } mModel.endDate.normalize(false); } togglePickerOptions(); updateDialog(); showRecurrencePicker(); } private void togglePickerOptions() { if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) { mFreqSpinner.setEnabled(false); mEndSpinner.setEnabled(false); mIntervalPreText.setEnabled(false); mInterval.setEnabled(false); mIntervalPostText.setEnabled(false); mMonthRepeatByRadioGroup.setEnabled(false); mEndCount.setEnabled(false); mPostEndCount.setEnabled(false); mEndDateTextView.setEnabled(false); mRepeatMonthlyByNthDayOfWeek.setEnabled(false); mRepeatMonthlyByNthDayOfMonth.setEnabled(false); for (Button button : mWeekByDayButtons) { button.setEnabled(false); } } else { findViewById(R.id.options).setEnabled(true); mFreqSpinner.setEnabled(true); mEndSpinner.setEnabled(true); mIntervalPreText.setEnabled(true); mInterval.setEnabled(true); mIntervalPostText.setEnabled(true); mMonthRepeatByRadioGroup.setEnabled(true); mEndCount.setEnabled(true); mPostEndCount.setEnabled(true); mEndDateTextView.setEnabled(true); mRepeatMonthlyByNthDayOfWeek.setEnabled(true); mRepeatMonthlyByNthDayOfMonth.setEnabled(true); for (Button button : mWeekByDayButtons) { button.setEnabled(true); } } updateDoneButtonState(); } private void updateDoneButtonState() { if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) { mButtonLayout.updateValidity(true); return; } if (mInterval.getText().toString().length() == 0) { mButtonLayout.updateValidity(false); return; } if (mEndCount.getVisibility() == View.VISIBLE && mEndCount.getText().toString().length() == 0) { mButtonLayout.updateValidity(false); return; } if (mModel.freq == RecurrenceModel.FREQ_WEEKLY) { for (CompoundButton b : mWeekByDayButtons) { if (b.isChecked()) { mButtonLayout.updateValidity(true); return; } } mButtonLayout.updateValidity(false); return; } mButtonLayout.updateValidity(true); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, mModel, mEndCount.hasFocus(), mRecurrencePicker.getVisibility() == View.VISIBLE ? CurrentView.RECURRENCE_PICKER : CurrentView.DATE_ONLY_PICKER); } @Override protected void onRestoreInstanceState(Parcelable state) { BaseSavedState bss = (BaseSavedState) state; super.onRestoreInstanceState(bss.getSuperState()); SavedState ss = (SavedState) bss; final boolean endCountHasFocus = ss.getEndCountHasFocus(); RecurrenceModel m = ss.getRecurrenceModel(); if (m != null) { mModel = m; } mRecurrence.wkst = EventRecurrence.timeDay2Day(RecurrenceUtils.getFirstDayOfWeek()); togglePickerOptions(); updateDialog(); if (ss.getCurrentView() == CurrentView.RECURRENCE_PICKER) { showRecurrencePicker(); post(new Runnable() { @Override public void run() { if (mEndCount != null && endCountHasFocus) { mEndCount.requestFocus(); } } }); } else { showDateOnlyPicker(); } } /** * Class for managing state storing/restoring. */ private static class SavedState extends View.BaseSavedState { private final RecurrenceModel mRecurrenceModel; private final boolean mEndCountHasFocus; private final CurrentView sCurrentView; /** * Constructor called from {@link DatePicker#onSaveInstanceState()} */ private SavedState(Parcelable superState, RecurrenceModel recurrenceModel, boolean endCountHasFocus, CurrentView currentView) { super(superState); mRecurrenceModel = recurrenceModel; mEndCountHasFocus = endCountHasFocus; sCurrentView = currentView; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mRecurrenceModel = in.readParcelable(RecurrenceModel.class.getClassLoader()); mEndCountHasFocus = in.readByte() != 0; sCurrentView = CurrentView.valueOf(in.readString()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeParcelable(mRecurrenceModel, flags); dest.writeByte((byte) (mEndCountHasFocus ? 1 : 0)); dest.writeString(sCurrentView.name()); } public RecurrenceModel getRecurrenceModel() { return mRecurrenceModel; } public boolean getEndCountHasFocus() { return mEndCountHasFocus; } public CurrentView getCurrentView() { return sCurrentView; } @SuppressWarnings("all") // suppress unused and hiding public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } 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); mWeekGroup.setVisibility(mModel.freq == RecurrenceModel.FREQ_WEEKLY ? View.VISIBLE : View.GONE); // mWeekGroup2 will be null when available width >= 460dp if (mWeekGroup2 != null) { mWeekGroup2.setVisibility(mModel.freq == RecurrenceModel.FREQ_WEEKLY ? View.VISIBLE : View.GONE); } mMonthRepeatByRadioGroup .setVisibility(mModel.freq == RecurrenceModel.FREQ_MONTHLY ? View.VISIBLE : View.GONE); switch (mModel.freq) { case RecurrenceModel.FREQ_DAILY: mIntervalResId = R.plurals.recurrence_interval_daily; break; case RecurrenceModel.FREQ_WEEKLY: mIntervalResId = R.plurals.recurrence_interval_weekly; for (int i = 0; i < 7; i++) { mWeekByDayButtons[i].setCheckedNoAnimate(mModel.weeklyByDayOfWeek[i]); } break; case RecurrenceModel.FREQ_MONTHLY: mIntervalResId = R.plurals.recurrence_interval_monthly; if (mModel.monthlyRepeat == RecurrenceModel.MONTHLY_BY_DATE) { mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfMonth); } else if (mModel.monthlyRepeat == RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK) { mMonthRepeatByRadioGroup.check(R.id.repeatMonthlyByNthDayOfTheWeek); } if (mMonthRepeatByDayOfWeekStr == null) { if (mModel.monthlyByNthDayOfWeek == 0) { mModel.monthlyByNthDayOfWeek = (mTime.monthDay + 6) / 7; // Since not all months have 5 weeks, we convert 5th NthDayOfWeek to // -1 for last monthly day of the week if (mModel.monthlyByNthDayOfWeek >= FIFTH_WEEK_IN_A_MONTH) { mModel.monthlyByNthDayOfWeek = LAST_NTH_DAY_OF_WEEK; } mModel.monthlyByDayOfWeek = mTime.weekDay; } String[] monthlyByNthDayOfWeekStrs = mMonthRepeatByDayOfWeekStrs[mModel.monthlyByDayOfWeek]; // TODO(psliwowski): Find a better way handle -1 indexes int msgIndex = mModel.monthlyByNthDayOfWeek < 0 ? FIFTH_WEEK_IN_A_MONTH : mModel.monthlyByNthDayOfWeek; mMonthRepeatByDayOfWeekStr = monthlyByNthDayOfWeekStrs[msgIndex - 1]; mRepeatMonthlyByNthDayOfWeek.setText(mMonthRepeatByDayOfWeekStr); } break; case RecurrenceModel.FREQ_YEARLY: mIntervalResId = R.plurals.recurrence_interval_yearly; break; } updateIntervalText(); updateDoneButtonState(); mEndSpinner.setSelection(mModel.end); if (mModel.end == RecurrenceModel.END_BY_DATE) { mEndDateTextView.setText(mEndDateFormatter.format(mModel.endDate.toMillis(false))); } else { if (mModel.end == RecurrenceModel.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); } } } } /** * @param endDateString String for end date option * displayed in End Spinner */ @SuppressWarnings("unused") private void setEndSpinnerEndDateStr(final String endDateString) { mEndSpinnerArray.set(1, endDateString); mEndSpinnerAdapter.notifyDataSetChanged(); } @SuppressWarnings("unused") private void doToast() { Log.e(TAG, "Model = " + mModel.toString()); String rrule; if (mModel.recurrenceState == RecurrenceModel.STATE_NO_RECURRENCE) { rrule = "Not repeating"; } else { copyModelToEventRecurrence(mModel, mRecurrence); rrule = mRecurrence.toString(); } if (mToast != null) { mToast.cancel(); } mToast = Toast.makeText(getContext(), rrule, Toast.LENGTH_LONG); mToast.show(); } // TODO Test and update for Right-to-Left private void updateIntervalText() { if (mIntervalResId == -1) { return; } final String INTERVAL_COUNT_MARKER = "%d"; String intervalString = mResources.getQuantityString(mIntervalResId, mModel.interval); int markerStart = intervalString.indexOf(INTERVAL_COUNT_MARKER); if (markerStart != -1) { int postTextStart = markerStart + INTERVAL_COUNT_MARKER.length(); mIntervalPostText.setText(intervalString.substring(postTextStart, intervalString.length()).trim()); mIntervalPreText.setText(intervalString.substring(0, markerStart).trim()); } } /** * Update the "Repeat for N events" end option with the proper string values * based on the value that has been entered for N. */ private void updateEndCountText() { final String END_COUNT_MARKER = "%d"; String endString = mResources.getQuantityString(R.plurals.recurrence_end_count, mModel.endCount); int markerStart = endString.indexOf(END_COUNT_MARKER); if (markerStart != -1) { if (markerStart == 0) { Log.e(TAG, "No text to put in to recurrence's end spinner."); } else { int postTextStart = markerStart + END_COUNT_MARKER.length(); mPostEndCount.setText(endString.substring(postTextStart, endString.length()).trim()); } } } // 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; } else if (parent == mEndSpinner) { switch (position) { case RecurrenceModel.END_NEVER: mModel.end = RecurrenceModel.END_NEVER; break; case RecurrenceModel.END_BY_DATE: mModel.end = RecurrenceModel.END_BY_DATE; break; case RecurrenceModel.END_BY_COUNT: mModel.end = RecurrenceModel.END_BY_COUNT; if (mModel.endCount <= 1) { mModel.endCount = 1; } else if (mModel.endCount > COUNT_MAX) { mModel.endCount = COUNT_MAX; } updateEndCountText(); break; } mEndCount.setVisibility(mModel.end == RecurrenceModel.END_BY_COUNT ? View.VISIBLE : View.GONE); mEndDateTextView.setVisibility(mModel.end == RecurrenceModel.END_BY_DATE ? View.VISIBLE : View.GONE); mPostEndCount.setVisibility( mModel.end == RecurrenceModel.END_BY_COUNT && !mHidePostEndCount ? View.VISIBLE : View.GONE); } updateDialog(); } // Implements OnItemSelectedListener interface @Override public void onNothingSelected(AdapterView<?> arg0) { } @Override public void onDateSet(RecurrenceEndDatePicker view, int year, int monthOfYear, int dayOfMonth) { showRecurrencePicker(); 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(); } @Override public void onDateOnlyPickerCancelled(RecurrenceEndDatePicker view) { showRecurrencePicker(); } // Implements OnCheckedChangeListener interface // Week repeat by day of week @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { int itemIdx = -1; for (int i = 0; i < 7; i++) { if (itemIdx == -1 && buttonView == mWeekByDayButtons[i]) { itemIdx = i; mModel.weeklyByDayOfWeek[i] = isChecked; } } 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 = RecurrenceModel.MONTHLY_BY_DATE; } else if (checkedId == R.id.repeatMonthlyByNthDayOfTheWeek) { mModel.monthlyRepeat = RecurrenceModel.MONTHLY_BY_NTH_DAY_OF_WEEK; } updateDialog(); } // Implements OnClickListener interface // EndDate button // Done button @Override public void onClick(View v) { if (mEndDateTextView == v) { showDateOnlyPicker(); } } private void showRecurrencePicker() { mDateOnlyPicker.setVisibility(View.GONE); mRecurrencePicker.setVisibility(View.VISIBLE); } private void showDateOnlyPicker() { mDateOnlyPicker.init(mModel.endDate.year, mModel.endDate.month, mModel.endDate.monthDay, this); mDateOnlyPicker.setFirstDayOfWeek(RecurrenceUtils.getFirstDayOfWeekAsCalendar()); mRecurrencePicker.setVisibility(View.GONE); mDateOnlyPicker.setVisibility(View.VISIBLE); } public interface OnRecurrenceSetListener { void onRecurrenceSet(String rrule); void onCancelled(); } private class EndSpinnerAdapter extends ArrayAdapter<CharSequence> { final String END_DATE_MARKER = "%s"; final String END_COUNT_MARKER = "%d"; private LayoutInflater mInflater; private int mItemLayoutId, mDropDownLayoutId, mTextResourceId; private ArrayList<CharSequence> mStrings; private String mEndDateString; private boolean mUseFormStrings; /** * @param context Context * @param strings {Forever, Until a date, For a number of events} * @param itemLayoutId @Layout resource used for displaying * selected option * @param textResourceId ViewID for the 'TextView' in 'itemLayoutId' * @param dropDownLayoutId @Layout resource used for displaying * available options in the dropdown menu */ public EndSpinnerAdapter(Context context, ArrayList<CharSequence> strings, int itemLayoutId, int textResourceId, int dropDownLayoutId) { super(context, itemLayoutId, strings); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mItemLayoutId = itemLayoutId; mTextResourceId = textResourceId; mDropDownLayoutId = dropDownLayoutId; mStrings = strings; mEndDateString = getResources().getString(R.string.recurrence_end_date); // If either date or count strings don't translate well, such that we aren't assured // to have some text available to be placed in the spinner, then we'll have to use // the more form-like versions of both strings instead. int markerStart = mEndDateString.indexOf(END_DATE_MARKER); if (markerStart <= 0) { // The date string does not have any text before the "%s" so we'll have to use the // more form-like strings instead. mUseFormStrings = true; } else { String countEndStr = getResources().getQuantityString(R.plurals.recurrence_end_count, 1); markerStart = countEndStr.indexOf(END_COUNT_MARKER); if (markerStart <= 0) { // The count string does not have any text before the "%d" so we'll have to use // the more form-like strings instead. mUseFormStrings = true; } } if (mUseFormStrings) { // We'll have to set the layout for the spinner to be weight=0 so it doesn't // take up too much space. mEndSpinner.setLayoutParams(new TableLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); } } @Override public View getView(int position, View convertView, ViewGroup parent) { View v; // Check if we can recycle the view if (convertView == null) { v = mInflater.inflate(mItemLayoutId, parent, false); } else { v = convertView; } TextView item = (TextView) v.findViewById(mTextResourceId); int markerStart; switch (position) { case RecurrenceModel.END_NEVER: item.setText(mStrings.get(RecurrenceModel.END_NEVER)); break; case RecurrenceModel.END_BY_DATE: markerStart = mEndDateString.indexOf(END_DATE_MARKER); if (markerStart != -1) { if (mUseFormStrings || markerStart == 0) { // If we get here, the translation of "Until" doesn't work correctly, // so we'll just set the whole "Until a date" string. item.setText(mEndDateLabel); } else { item.setText(mEndDateString.substring(0, markerStart).trim()); } } break; case RecurrenceModel.END_BY_COUNT: String endString = mResources.getQuantityString(R.plurals.recurrence_end_count, mModel.endCount); markerStart = endString.indexOf(END_COUNT_MARKER); if (markerStart != -1) { if (mUseFormStrings || markerStart == 0) { // If we get here, the translation of "For" doesn't work correctly, // so we'll just set the whole "For a number of events" string. item.setText(mEndCountLabel); // Also, we'll hide the " events" that would have been at the end. mPostEndCount.setVisibility(View.GONE); // Use this flag so the onItemSelected knows whether to show it later. mHidePostEndCount = true; } else { int postTextStart = markerStart + END_COUNT_MARKER.length(); mPostEndCount.setText(endString.substring(postTextStart, endString.length()).trim()); // In case it's a recycled view that wasn't visible. if (mModel.end == RecurrenceModel.END_BY_COUNT) { mPostEndCount.setVisibility(View.VISIBLE); } if (endString.charAt(markerStart - 1) == ' ') { markerStart--; } item.setText(endString.substring(0, markerStart).trim()); } } break; default: v = null; break; } return v; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { View v; // Check if we can recycle the view if (convertView == null) { v = mInflater.inflate(mDropDownLayoutId, parent, false); } else { v = convertView; } TextView item = (TextView) v.findViewById(mTextResourceId); item.setText(mStrings.get(position)); return v; } } }