• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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;
18 
19 import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
20 import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME;
21 import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH;
22 
23 import com.android.calendar.CalendarController.EventInfo;
24 import com.android.calendar.CalendarController.EventType;
25 import com.android.calendar.CalendarEventModel.Attendee;
26 import com.android.calendar.CalendarEventModel.ReminderEntry;
27 import com.android.calendar.event.AttendeesView;
28 import com.android.calendar.event.EditEventActivity;
29 import com.android.calendar.event.EditEventHelper;
30 import com.android.calendar.event.EventViewUtils;
31 import com.android.calendarcommon.EventRecurrence;
32 import com.android.i18n.phonenumbers.PhoneNumberMatch;
33 import com.android.i18n.phonenumbers.PhoneNumberUtil;
34 
35 import android.app.Activity;
36 import android.app.Dialog;
37 import android.app.DialogFragment;
38 import android.app.Service;
39 import android.content.ActivityNotFoundException;
40 import android.content.ContentProviderOperation;
41 import android.content.ContentResolver;
42 import android.content.ContentUris;
43 import android.content.ContentValues;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.SharedPreferences;
47 import android.content.res.Resources;
48 import android.database.Cursor;
49 import android.graphics.Rect;
50 import android.graphics.Typeface;
51 import android.net.Uri;
52 import android.os.Bundle;
53 import android.provider.CalendarContract;
54 import android.provider.CalendarContract.Attendees;
55 import android.provider.CalendarContract.Calendars;
56 import android.provider.CalendarContract.Events;
57 import android.provider.CalendarContract.Reminders;
58 import android.provider.ContactsContract;
59 import android.provider.ContactsContract.CommonDataKinds;
60 import android.provider.ContactsContract.Intents;
61 import android.provider.ContactsContract.QuickContact;
62 import android.text.Spannable;
63 import android.text.SpannableString;
64 import android.text.SpannableStringBuilder;
65 import android.text.Spanned;
66 import android.text.TextUtils;
67 import android.text.format.DateFormat;
68 import android.text.format.DateUtils;
69 import android.text.format.Time;
70 import android.text.method.LinkMovementMethod;
71 import android.text.method.MovementMethod;
72 import android.text.style.ForegroundColorSpan;
73 import android.text.style.StrikethroughSpan;
74 import android.text.style.StyleSpan;
75 import android.text.style.URLSpan;
76 import android.text.util.Linkify;
77 import android.text.util.Rfc822Token;
78 import android.util.Log;
79 import android.view.Gravity;
80 import android.view.LayoutInflater;
81 import android.view.Menu;
82 import android.view.MenuInflater;
83 import android.view.MenuItem;
84 import android.view.MotionEvent;
85 import android.view.View;
86 import android.view.View.OnClickListener;
87 import android.view.View.OnTouchListener;
88 import android.view.ViewGroup;
89 import android.view.Window;
90 import android.view.WindowManager;
91 import android.view.accessibility.AccessibilityEvent;
92 import android.view.accessibility.AccessibilityManager;
93 import android.widget.AdapterView;
94 import android.widget.AdapterView.OnItemSelectedListener;
95 import android.widget.Button;
96 import android.widget.LinearLayout;
97 import android.widget.RadioButton;
98 import android.widget.RadioGroup;
99 import android.widget.RadioGroup.OnCheckedChangeListener;
100 import android.widget.ScrollView;
101 import android.widget.TextView;
102 import android.widget.Toast;
103 
104 import java.util.ArrayList;
105 import java.util.Arrays;
106 import java.util.Collections;
107 import java.util.Formatter;
108 import java.util.List;
109 import java.util.Locale;
110 import java.util.TimeZone;
111 import java.util.regex.Pattern;
112 
113 
114 public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
115         CalendarController.EventHandler, OnClickListener {
116     public static final boolean DEBUG = false;
117 
118     public static final String TAG = "EventInfoFragment";
119 
120     protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
121     protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
122     protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
123     protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
124     protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response";
125 
126     private static final String PERIOD_SPACE = ". ";
127 
128     /**
129      * These are the corresponding indices into the array of strings
130      * "R.array.change_response_labels" in the resource file.
131      */
132     static final int UPDATE_SINGLE = 0;
133     static final int UPDATE_ALL = 1;
134 
135     // Query tokens for QueryHandler
136     private static final int TOKEN_QUERY_EVENT = 1 << 0;
137     private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
138     private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
139     private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
140     private static final int TOKEN_QUERY_REMINDERS = 1 << 4;
141     private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
142             | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT
143             | TOKEN_QUERY_REMINDERS;
144     private int mCurrentQuery = 0;
145 
146     private static final String[] EVENT_PROJECTION = new String[] {
147         Events._ID,                  // 0  do not remove; used in DeleteEventHelper
148         Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
149         Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
150         Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
151         Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
152         Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
153         Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
154         Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
155         Events.DESCRIPTION,          // 8
156         Events.EVENT_LOCATION,       // 9
157         Calendars.CALENDAR_ACCESS_LEVEL,      // 10
158         Calendars.CALENDAR_COLOR,             // 11
159         Events.HAS_ATTENDEE_DATA,    // 12
160         Events.ORGANIZER,            // 13
161         Events.HAS_ALARM,            // 14
162         Calendars.MAX_REMINDERS,     //15
163         Calendars.ALLOWED_REMINDERS, // 16
164         Events.ORIGINAL_SYNC_ID,     // 17 do not remove; used in DeleteEventHelper
165     };
166     private static final int EVENT_INDEX_ID = 0;
167     private static final int EVENT_INDEX_TITLE = 1;
168     private static final int EVENT_INDEX_RRULE = 2;
169     private static final int EVENT_INDEX_ALL_DAY = 3;
170     private static final int EVENT_INDEX_CALENDAR_ID = 4;
171     private static final int EVENT_INDEX_SYNC_ID = 6;
172     private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
173     private static final int EVENT_INDEX_DESCRIPTION = 8;
174     private static final int EVENT_INDEX_EVENT_LOCATION = 9;
175     private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
176     private static final int EVENT_INDEX_COLOR = 11;
177     private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12;
178     private static final int EVENT_INDEX_ORGANIZER = 13;
179     private static final int EVENT_INDEX_HAS_ALARM = 14;
180     private static final int EVENT_INDEX_MAX_REMINDERS = 15;
181     private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16;
182 
183 
184     private static final String[] ATTENDEES_PROJECTION = new String[] {
185         Attendees._ID,                      // 0
186         Attendees.ATTENDEE_NAME,            // 1
187         Attendees.ATTENDEE_EMAIL,           // 2
188         Attendees.ATTENDEE_RELATIONSHIP,    // 3
189         Attendees.ATTENDEE_STATUS,          // 4
190     };
191     private static final int ATTENDEES_INDEX_ID = 0;
192     private static final int ATTENDEES_INDEX_NAME = 1;
193     private static final int ATTENDEES_INDEX_EMAIL = 2;
194     private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
195     private static final int ATTENDEES_INDEX_STATUS = 4;
196 
197     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
198 
199     private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
200             + Attendees.ATTENDEE_EMAIL + " ASC";
201 
202     private static final String[] REMINDERS_PROJECTION = new String[] {
203         Reminders._ID,                      // 0
204         Reminders.MINUTES,            // 1
205         Reminders.METHOD           // 2
206     };
207     private static final int REMINDERS_INDEX_ID = 0;
208     private static final int REMINDERS_MINUTES_ID = 1;
209     private static final int REMINDERS_METHOD_ID = 2;
210 
211     private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
212 
213     static final String[] CALENDARS_PROJECTION = new String[] {
214         Calendars._ID,           // 0
215         Calendars.CALENDAR_DISPLAY_NAME,  // 1
216         Calendars.OWNER_ACCOUNT, // 2
217         Calendars.CAN_ORGANIZER_RESPOND // 3
218     };
219     static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
220     static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
221     static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
222 
223     static final String CALENDARS_WHERE = Calendars._ID + "=?";
224     static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
225 
226     private View mView;
227 
228     private Uri mUri;
229     private long mEventId;
230     private Cursor mEventCursor;
231     private Cursor mAttendeesCursor;
232     private Cursor mCalendarsCursor;
233     private Cursor mRemindersCursor;
234 
235     private static float mScale = 0; // Used for supporting different screen densities
236 
237     private long mStartMillis;
238     private long mEndMillis;
239 
240     private boolean mHasAttendeeData;
241     private boolean mIsOrganizer;
242     private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
243     private boolean mOwnerCanRespond;
244     private String mCalendarOwnerAccount;
245     private boolean mCanModifyCalendar;
246     private boolean mCanModifyEvent;
247     private boolean mIsBusyFreeCalendar;
248     private int mNumOfAttendees;
249 
250     private EditResponseHelper mEditResponseHelper;
251 
252     private int mOriginalAttendeeResponse;
253     private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE;
254     private int mUserSetResponse = CalendarController.ATTENDEE_NO_RESPONSE;
255     private boolean mIsRepeating;
256     private boolean mHasAlarm;
257     private int mMaxReminders;
258     private String mCalendarAllowedReminders;
259 
260     private TextView mTitle;
261     private TextView mWhenDate;
262     private TextView mWhenTime;
263     private TextView mWhere;
264     private TextView mDesc;
265     private AttendeesView mLongAttendees;
266     private Menu mMenu = null;
267     private View mHeadlines;
268     private ScrollView mScrollView;
269 
270     private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
271 
272     ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
273     ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
274     ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
275     ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
276     private int mColor;
277 
278 
279     private int mDefaultReminderMinutes;
280     private ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0);
281     public ArrayList<ReminderEntry> mReminders;
282     public ArrayList<ReminderEntry> mOriginalReminders = new ArrayList<ReminderEntry>();
283     public ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>();
284     private boolean mUserModifiedReminders = false;
285 
286     /**
287      * Contents of the "minutes" spinner.  This has default values from the XML file, augmented
288      * with any additional values that were already associated with the event.
289      */
290     private ArrayList<Integer> mReminderMinuteValues;
291     private ArrayList<String> mReminderMinuteLabels;
292 
293     /**
294      * Contents of the "methods" spinner.  The "values" list specifies the method constant
295      * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels.  Any methods that
296      * aren't allowed by the Calendar will be removed.
297      */
298     private ArrayList<Integer> mReminderMethodValues;
299     private ArrayList<String> mReminderMethodLabels;
300 
301     private QueryHandler mHandler;
302 
303     private Runnable mTZUpdater = new Runnable() {
304         @Override
305         public void run() {
306             updateEvent(mView);
307         }
308     };
309 
310     private OnItemSelectedListener mReminderChangeListener;
311 
312     private static int DIALOG_WIDTH = 500;
313     private static int DIALOG_HEIGHT = 600;
314     private static int DIALOG_TOP_MARGIN = 8;
315     private boolean mIsDialog = false;
316     private boolean mIsPaused = true;
317     private boolean mDismissOnResume = false;
318     private int mX = -1;
319     private int mY = -1;
320     private int mMinTop;         // Dialog cannot be above this location
321     private Button mDescButton;  // Button to expand/collapse the description
322     private String mMoreLabel;   // Labels for the button
323     private String mLessLabel;
324     private boolean mShowMaxDescription;  // Current status of button
325     private int mDescLineNum;             // The default number of lines in the description
326     private boolean mIsTabletConfig;
327     private Activity mActivity;
328     private Context mContext;
329 
330     private class QueryHandler extends AsyncQueryService {
QueryHandler(Context context)331         public QueryHandler(Context context) {
332             super(context);
333         }
334 
335         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)336         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
337             // if the activity is finishing, then close the cursor and return
338             final Activity activity = getActivity();
339             if (activity == null || activity.isFinishing()) {
340                 cursor.close();
341                 return;
342             }
343 
344             switch (token) {
345             case TOKEN_QUERY_EVENT:
346                 mEventCursor = Utils.matrixCursorFromCursor(cursor);
347                 if (initEventCursor()) {
348                     // The cursor is empty. This can happen if the event was
349                     // deleted.
350                     // FRAG_TODO we should no longer rely on Activity.finish()
351                     activity.finish();
352                     return;
353                 }
354                 updateEvent(mView);
355                 prepareReminders();
356 
357                 // start calendar query
358                 Uri uri = Calendars.CONTENT_URI;
359                 String[] args = new String[] {
360                         Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
361                 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
362                         CALENDARS_WHERE, args, null);
363                 break;
364             case TOKEN_QUERY_CALENDARS:
365                 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
366                 updateCalendar(mView);
367                 // FRAG_TODO fragments shouldn't set the title anymore
368                 updateTitle();
369 
370                 if (!mIsBusyFreeCalendar) {
371                     args = new String[] { Long.toString(mEventId) };
372 
373                     // start attendees query
374                     uri = Attendees.CONTENT_URI;
375                     startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
376                             ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
377                 } else {
378                     sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
379                 }
380                 if (mHasAlarm) {
381                     // start reminders query
382                     args = new String[] { Long.toString(mEventId) };
383                     uri = Reminders.CONTENT_URI;
384                     startQuery(TOKEN_QUERY_REMINDERS, null, uri,
385                             REMINDERS_PROJECTION, REMINDERS_WHERE, args, null);
386                 } else {
387                     sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS);
388                 }
389                 break;
390             case TOKEN_QUERY_ATTENDEES:
391                 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
392                 initAttendeesCursor(mView);
393                 updateResponse(mView);
394                 break;
395             case TOKEN_QUERY_REMINDERS:
396                 mRemindersCursor = Utils.matrixCursorFromCursor(cursor);
397                 initReminders(mView, mRemindersCursor);
398                 break;
399             case TOKEN_QUERY_DUPLICATE_CALENDARS:
400                 Resources res = activity.getResources();
401                 SpannableStringBuilder sb = new SpannableStringBuilder();
402 
403                 // Label
404                 String label = res.getString(R.string.view_event_calendar_label);
405                 sb.append(label).append(" ");
406                 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(),
407                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
408 
409                 // Calendar display name
410                 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
411                 sb.append(calendarName);
412 
413                 // Show email account if display name is not unique and
414                 // display name != email
415                 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
416                 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) {
417                     sb.append(" (").append(email).append(")");
418                 }
419 
420                 break;
421             }
422             cursor.close();
423             sendAccessibilityEventIfQueryDone(token);
424         }
425 
426     }
427 
sendAccessibilityEventIfQueryDone(int token)428     private void sendAccessibilityEventIfQueryDone(int token) {
429         mCurrentQuery |= token;
430         if (mCurrentQuery == TOKEN_QUERY_ALL) {
431             sendAccessibilityEvent();
432         }
433     }
434 
EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, int attendeeResponse, boolean isDialog)435     public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
436             int attendeeResponse, boolean isDialog) {
437         if (mScale == 0) {
438             mScale = context.getResources().getDisplayMetrics().density;
439             if (mScale != 1) {
440                 DIALOG_WIDTH *= mScale;
441                 DIALOG_HEIGHT *= mScale;
442                 DIALOG_TOP_MARGIN *= mScale;
443             }
444         }
445         mIsDialog = isDialog;
446 
447         setStyle(DialogFragment.STYLE_NO_TITLE, 0);
448         mUri = uri;
449         mStartMillis = startMillis;
450         mEndMillis = endMillis;
451         mAttendeeResponseFromIntent = attendeeResponse;
452     }
453 
454     // This is currently required by the fragment manager.
EventInfoFragment()455     public EventInfoFragment() {
456     }
457 
458 
459 
EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, int attendeeResponse, boolean isDialog)460     public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
461             int attendeeResponse, boolean isDialog) {
462         this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
463                 endMillis, attendeeResponse, isDialog);
464         mEventId = eventId;
465     }
466 
467     @Override
onActivityCreated(Bundle savedInstanceState)468     public void onActivityCreated(Bundle savedInstanceState) {
469         super.onActivityCreated(savedInstanceState);
470 
471         mReminderChangeListener = new OnItemSelectedListener() {
472             @Override
473             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
474                 Integer prevValue = (Integer) parent.getTag();
475                 if (prevValue == null || prevValue != position) {
476                     parent.setTag(position);
477                     mUserModifiedReminders = true;
478                 }
479             }
480 
481             @Override
482             public void onNothingSelected(AdapterView<?> parent) {
483                 // do nothing
484             }
485 
486         };
487 
488         if (savedInstanceState != null) {
489             mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
490         }
491 
492         if (mIsDialog) {
493             applyDialogParams();
494         }
495         mContext = getActivity();
496     }
497 
applyDialogParams()498     private void applyDialogParams() {
499         Dialog dialog = getDialog();
500         dialog.setCanceledOnTouchOutside(true);
501 
502         Window window = dialog.getWindow();
503         window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
504 
505         WindowManager.LayoutParams a = window.getAttributes();
506         a.dimAmount = .4f;
507 
508         a.width = DIALOG_WIDTH;
509         a.height = DIALOG_HEIGHT;
510 
511 
512         // On tablets , do smart positioning of dialog
513         // On phones , use the whole screen
514 
515         if (mX != -1 || mY != -1) {
516             a.x = mX - DIALOG_WIDTH / 2;
517             a.y = mY - DIALOG_HEIGHT / 2;
518             if (a.y < mMinTop) {
519                 a.y = mMinTop + DIALOG_TOP_MARGIN;
520             }
521             a.gravity = Gravity.LEFT | Gravity.TOP;
522         }
523         window.setAttributes(a);
524     }
525 
setDialogParams(int x, int y, int minTop)526     public void setDialogParams(int x, int y, int minTop) {
527         mX = x;
528         mY = y;
529         mMinTop = minTop;
530     }
531 
532     // Implements OnCheckedChangeListener
533     @Override
onCheckedChanged(RadioGroup group, int checkedId)534     public void onCheckedChanged(RadioGroup group, int checkedId) {
535         // If this is not a repeating event, then don't display the dialog
536         // asking which events to change.
537         mUserSetResponse = getResponseFromButtonId(checkedId);
538         if (!mIsRepeating) {
539             return;
540         }
541 
542         // If the selection is the same as the original, then don't display the
543         // dialog asking which events to change.
544         if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
545             return;
546         }
547 
548         // This is a repeating event. We need to ask the user if they mean to
549         // change just this one instance or all instances.
550         mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
551     }
552 
onNothingSelected(AdapterView<?> parent)553     public void onNothingSelected(AdapterView<?> parent) {
554     }
555 
556     @Override
onAttach(Activity activity)557     public void onAttach(Activity activity) {
558         super.onAttach(activity);
559         mActivity = activity;
560         mEditResponseHelper = new EditResponseHelper(activity);
561 
562         if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
563             mEditResponseHelper.setWhichEvents(UPDATE_ALL);
564         }
565         mHandler = new QueryHandler(activity);
566         mDescLineNum = activity.getResources().getInteger((R.integer.event_info_desc_line_num));
567         mMoreLabel = activity.getResources().getString((R.string.event_info_desc_more));
568         mLessLabel = activity.getResources().getString((R.string.event_info_desc_less));
569         if (!mIsDialog) {
570             setHasOptionsMenu(true);
571         }
572     }
573 
574     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)575     public View onCreateView(LayoutInflater inflater, ViewGroup container,
576             Bundle savedInstanceState) {
577         mView = inflater.inflate(R.layout.event_info, container, false);
578         mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view);
579         mTitle = (TextView) mView.findViewById(R.id.title);
580         mWhenDate = (TextView) mView.findViewById(R.id.when_date);
581         mWhenTime = (TextView) mView.findViewById(R.id.when_time);
582         mWhere = (TextView) mView.findViewById(R.id.where);
583         mDesc = (TextView) mView.findViewById(R.id.description);
584         mHeadlines = mView.findViewById(R.id.event_info_headline);
585         mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list);
586         mDescButton = (Button)mView.findViewById(R.id.desc_expand);
587         mDescButton.setOnClickListener(new View.OnClickListener() {
588             @Override
589             public void onClick(View v) {
590                 mShowMaxDescription = !mShowMaxDescription;
591                 updateDescription();
592             }
593         });
594         mShowMaxDescription = false; // Show short version of description as default.
595         mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config);
596 
597         if (mUri == null) {
598             // restore event ID from bundle
599             mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
600             mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
601             mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
602             mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
603         }
604 
605         // start loading the data
606         mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
607                 null, null, null);
608 
609         Button b = (Button) mView.findViewById(R.id.delete);
610         b.setOnClickListener(new OnClickListener() {
611             @Override
612             public void onClick(View v) {
613                 if (!mCanModifyCalendar) {
614                     return;
615                 }
616                 DeleteEventHelper deleteHelper = new DeleteEventHelper(
617                         mContext, mActivity,
618                         !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
619                 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
620             }});
621 
622         // Hide Edit/Delete buttons if in full screen mode on a phone
623         if (savedInstanceState != null) {
624             mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
625         }
626         if (!mIsDialog && !mIsTabletConfig) {
627             mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE);
628         }
629 
630         // Create a listener for the add reminder button
631 
632         View reminderAddButton = mView.findViewById(R.id.reminder_add);
633         View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
634             @Override
635             public void onClick(View v) {
636                 addReminder();
637                 mUserModifiedReminders = true;
638             }
639         };
640         reminderAddButton.setOnClickListener(addReminderOnClickListener);
641 
642         // Set reminders variables
643 
644         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
645         String defaultReminderString = prefs.getString(
646                 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
647         mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
648         prepareReminders();
649 
650         return mView;
651     }
652 
653     private Runnable onDeleteRunnable = new Runnable() {
654         @Override
655         public void run() {
656             if (EventInfoFragment.this.mIsPaused) {
657                 mDismissOnResume = true;
658                 return;
659             }
660             if (EventInfoFragment.this.isVisible()) {
661                 EventInfoFragment.this.dismiss();
662             }
663         }
664     };
665 
666     // Sets the description:
667     // Set the expand/collapse button
668     // Expand/collapse the description according the the current status
updateDescription()669     private void updateDescription() {
670         // If there is no description, hide the description field
671         // and desc button.
672         String text = mDesc.getText().toString();
673         if (TextUtils.isEmpty(text) || TextUtils.isEmpty(text.trim())) {
674             mDesc.setVisibility(View.GONE);
675             mDescButton.setVisibility(View.GONE);
676             return;
677         }
678         // getLineCount() returns at most maxLines worth of text. If we have
679         // less than mDescLineNum lines, we know for sure we don't need the
680         // more/less button and we don't need to recalculate the number of
681         // lines.
682 
683         mDesc.setVisibility(View.VISIBLE);
684 
685         if (mDesc.getLineCount() < mDescLineNum) {
686             mDescButton.setVisibility(View.GONE);
687             return;
688         }
689 
690         // getLineCount() returns at most maxLines worth of text. To
691         // recalculate, set to MAX_VALUE.
692         mDesc.setMaxLines(Integer.MAX_VALUE);
693 
694         // Trick to get textview to recalculate line count
695         mDesc.setText(mDesc.getText());
696 
697         // Description is exactly mDescLineNum lines (or less).
698         if (mDesc.getLineCount() <= mDescLineNum) {
699             mDescButton.setVisibility(View.GONE);
700             return;
701         }
702 
703         // Show button and set label according to the expand/collapse status
704         mDescButton.setVisibility(View.VISIBLE);
705         String moreLessLabel;
706         if (mShowMaxDescription) {
707             moreLessLabel = mLessLabel;
708         } else {
709             moreLessLabel = mMoreLabel;
710             mDesc.setMaxLines(mDescLineNum);
711         }
712 
713         mDescButton.setText(moreLessLabel);
714     }
715 
updateTitle()716     private void updateTitle() {
717         Resources res = getActivity().getResources();
718         if (mCanModifyCalendar && !mIsOrganizer) {
719             getActivity().setTitle(res.getString(R.string.event_info_title_invite));
720         } else {
721             getActivity().setTitle(res.getString(R.string.event_info_title));
722         }
723     }
724 
725     /**
726      * Initializes the event cursor, which is expected to point to the first
727      * (and only) result from a query.
728      * @return true if the cursor is empty.
729      */
initEventCursor()730     private boolean initEventCursor() {
731         if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
732             return true;
733         }
734         mEventCursor.moveToFirst();
735         mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
736         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
737         mIsRepeating = !TextUtils.isEmpty(rRule);
738         mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false;
739         mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS);
740         mCalendarAllowedReminders =  mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS);
741         return false;
742     }
743 
744     @SuppressWarnings("fallthrough")
initAttendeesCursor(View view)745     private void initAttendeesCursor(View view) {
746         mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE;
747         mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
748         mNumOfAttendees = 0;
749         if (mAttendeesCursor != null) {
750             mNumOfAttendees = mAttendeesCursor.getCount();
751             if (mAttendeesCursor.moveToFirst()) {
752                 mAcceptedAttendees.clear();
753                 mDeclinedAttendees.clear();
754                 mTentativeAttendees.clear();
755                 mNoResponseAttendees.clear();
756 
757                 do {
758                     int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
759                     String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
760                     String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
761 
762                     if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
763                             mCalendarOwnerAccount.equalsIgnoreCase(email)) {
764                         mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
765                         mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
766                     } else {
767                         // Don't show your own status in the list because:
768                         //  1) it doesn't make sense for event without other guests.
769                         //  2) there's a spinner for that for events with guests.
770                         switch(status) {
771                             case Attendees.ATTENDEE_STATUS_ACCEPTED:
772                                 mAcceptedAttendees.add(new Attendee(name, email,
773                                         Attendees.ATTENDEE_STATUS_ACCEPTED));
774                                 break;
775                             case Attendees.ATTENDEE_STATUS_DECLINED:
776                                 mDeclinedAttendees.add(new Attendee(name, email,
777                                         Attendees.ATTENDEE_STATUS_DECLINED));
778                                 break;
779                             case Attendees.ATTENDEE_STATUS_TENTATIVE:
780                                 mTentativeAttendees.add(new Attendee(name, email,
781                                         Attendees.ATTENDEE_STATUS_TENTATIVE));
782                                 break;
783                             default:
784                                 mNoResponseAttendees.add(new Attendee(name, email,
785                                         Attendees.ATTENDEE_STATUS_NONE));
786                         }
787                     }
788                 } while (mAttendeesCursor.moveToNext());
789                 mAttendeesCursor.moveToFirst();
790 
791                 updateAttendees(view);
792             }
793         }
794     }
795 
796     @Override
onSaveInstanceState(Bundle outState)797     public void onSaveInstanceState(Bundle outState) {
798         super.onSaveInstanceState(outState);
799         outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
800         outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
801         outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
802         outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
803         outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent);
804     }
805 
806 
807     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)808     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
809         super.onCreateOptionsMenu(menu, inflater);
810         // Show edit/delete buttons only in non-dialog configuration on a phone
811         if (!mIsDialog && !mIsTabletConfig) {
812             inflater.inflate(R.menu.event_info_title_bar, menu);
813             mMenu = menu;
814             updateMenu();
815         }
816     }
817 
818     @Override
onOptionsItemSelected(MenuItem item)819     public boolean onOptionsItemSelected(MenuItem item) {
820 
821         // If we're a dialog or part of a tablet display we don't want to handle
822         // menu buttons
823         if (mIsDialog || mIsTabletConfig) {
824             return false;
825         }
826         // Handles option menu selections:
827         // Home button - close event info activity and start the main calendar
828         // one
829         // Edit button - start the event edit activity and close the info
830         // activity
831         // Delete button - start a delete query that calls a runnable that close
832         // the info activity
833 
834         switch (item.getItemId()) {
835             case android.R.id.home:
836                 Utils.returnToCalendarHome(mContext);
837                 mActivity.finish();
838                 return true;
839             case R.id.info_action_edit:
840                 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
841                 Intent intent = new Intent(Intent.ACTION_EDIT, uri);
842                 intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
843                 intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis);
844                 intent.setClass(mActivity, EditEventActivity.class);
845                 intent.putExtra(EVENT_EDIT_ON_LAUNCH, true);
846                 startActivity(intent);
847                 mActivity.finish();
848                 break;
849             case R.id.info_action_delete:
850                 DeleteEventHelper deleteHelper =
851                         new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */);
852                 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
853                 break;
854             default:
855                 break;
856         }
857         return super.onOptionsItemSelected(item);
858     }
859 
860     @Override
onDestroyView()861     public void onDestroyView() {
862         if (saveResponse() || saveReminders()) {
863             Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
864         }
865         super.onDestroyView();
866     }
867 
868     @Override
onDestroy()869     public void onDestroy() {
870         if (mEventCursor != null) {
871             mEventCursor.close();
872         }
873         if (mCalendarsCursor != null) {
874             mCalendarsCursor.close();
875         }
876         if (mAttendeesCursor != null) {
877             mAttendeesCursor.close();
878         }
879         super.onDestroy();
880     }
881 
882     /**
883      * Asynchronously saves the response to an invitation if the user changed
884      * the response. Returns true if the database will be updated.
885      *
886      * @return true if the database will be changed
887      */
saveResponse()888     private boolean saveResponse() {
889         if (mAttendeesCursor == null || mEventCursor == null) {
890             return false;
891         }
892 
893         RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value);
894         int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId());
895         if (status == Attendees.ATTENDEE_STATUS_NONE) {
896             return false;
897         }
898 
899         // If the status has not changed, then don't update the database
900         if (status == mOriginalAttendeeResponse) {
901             return false;
902         }
903 
904         // If we never got an owner attendee id we can't set the status
905         if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
906             return false;
907         }
908 
909         if (!mIsRepeating) {
910             // This is a non-repeating event
911             updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
912             return true;
913         }
914 
915         // This is a repeating event
916         int whichEvents = mEditResponseHelper.getWhichEvents();
917         switch (whichEvents) {
918             case -1:
919                 return false;
920             case UPDATE_SINGLE:
921                 createExceptionResponse(mEventId, status);
922                 return true;
923             case UPDATE_ALL:
924                 updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
925                 return true;
926             default:
927                 Log.e(TAG, "Unexpected choice for updating invitation response");
928                 break;
929         }
930         return false;
931     }
932 
updateResponse(long eventId, long attendeeId, int status)933     private void updateResponse(long eventId, long attendeeId, int status) {
934         // Update the attendee status in the attendees table.  the provider
935         // takes care of updating the self attendance status.
936         ContentValues values = new ContentValues();
937 
938         if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
939             values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
940         }
941         values.put(Attendees.ATTENDEE_STATUS, status);
942         values.put(Attendees.EVENT_ID, eventId);
943 
944         Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
945 
946         mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
947                 null, null, Utils.UNDO_DELAY);
948     }
949 
950     /**
951      * Creates an exception to a recurring event.  The only change we're making is to the
952      * "self attendee status" value.  The provider will take care of updating the corresponding
953      * Attendees.attendeeStatus entry.
954      *
955      * @param eventId The recurring event.
956      * @param status The new value for selfAttendeeStatus.
957      */
createExceptionResponse(long eventId, int status)958     private void createExceptionResponse(long eventId, int status) {
959         ContentValues values = new ContentValues();
960         values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
961         values.put(Events.SELF_ATTENDEE_STATUS, status);
962         values.put(Events.STATUS, Events.STATUS_CONFIRMED);
963 
964         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
965         Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
966                 String.valueOf(eventId));
967         ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build());
968 
969         mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops,
970                 Utils.UNDO_DELAY);
971    }
972 
getResponseFromButtonId(int buttonId)973     public static int getResponseFromButtonId(int buttonId) {
974         int response;
975         switch (buttonId) {
976             case R.id.response_yes:
977                 response = Attendees.ATTENDEE_STATUS_ACCEPTED;
978                 break;
979             case R.id.response_maybe:
980                 response = Attendees.ATTENDEE_STATUS_TENTATIVE;
981                 break;
982             case R.id.response_no:
983                 response = Attendees.ATTENDEE_STATUS_DECLINED;
984                 break;
985             default:
986                 response = Attendees.ATTENDEE_STATUS_NONE;
987         }
988         return response;
989     }
990 
findButtonIdForResponse(int response)991     public static int findButtonIdForResponse(int response) {
992         int buttonId;
993         switch (response) {
994             case Attendees.ATTENDEE_STATUS_ACCEPTED:
995                 buttonId = R.id.response_yes;
996                 break;
997             case Attendees.ATTENDEE_STATUS_TENTATIVE:
998                 buttonId = R.id.response_maybe;
999                 break;
1000             case Attendees.ATTENDEE_STATUS_DECLINED:
1001                 buttonId = R.id.response_no;
1002                 break;
1003                 default:
1004                     buttonId = -1;
1005         }
1006         return buttonId;
1007     }
1008 
doEdit()1009     private void doEdit() {
1010         Context c = getActivity();
1011         // This ensures that we aren't in the process of closing and have been
1012         // unattached already
1013         if (c != null) {
1014             CalendarController.getInstance(c).sendEventRelatedEvent(
1015                     this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0
1016                     , 0, -1);
1017         }
1018     }
1019 
updateEvent(View view)1020     private void updateEvent(View view) {
1021         if (mEventCursor == null || view == null) {
1022             return;
1023         }
1024 
1025         String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
1026         if (eventName == null || eventName.length() == 0) {
1027             eventName = getActivity().getString(R.string.no_title_label);
1028         }
1029 
1030         boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1031         String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
1032         String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
1033         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
1034         String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
1035 
1036         mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR));
1037         mHeadlines.setBackgroundColor(mColor);
1038 
1039         // What
1040         if (eventName != null) {
1041             setTextCommon(view, R.id.title, eventName);
1042         }
1043 
1044         // When
1045         // Set the date and repeats (if any)
1046         String whenDate;
1047         int flagsTime = DateUtils.FORMAT_SHOW_TIME;
1048         int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY |
1049                 DateUtils.FORMAT_SHOW_YEAR;
1050 
1051         if (DateFormat.is24HourFormat(getActivity())) {
1052             flagsTime |= DateUtils.FORMAT_24HOUR;
1053         }
1054 
1055         // Put repeat after the date (if any)
1056         String repeatString = null;
1057         if (!TextUtils.isEmpty(rRule)) {
1058             EventRecurrence eventRecurrence = new EventRecurrence();
1059             eventRecurrence.parse(rRule);
1060             Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
1061             if (allDay) {
1062                 date.timezone = Time.TIMEZONE_UTC;
1063             }
1064             date.set(mStartMillis);
1065             eventRecurrence.setStartDate(date);
1066             repeatString = EventRecurrenceFormatter.getRepeatString(
1067                     getActivity().getResources(), eventRecurrence);
1068         }
1069         // If an all day event , show the date without the time
1070         if (allDay) {
1071             Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault());
1072             whenDate = DateUtils.formatDateRange(getActivity(), f, mStartMillis, mEndMillis,
1073                     flagsDate, Time.TIMEZONE_UTC).toString();
1074             if (repeatString != null) {
1075                 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")");
1076             } else {
1077                 setTextCommon(view, R.id.when_date, whenDate);
1078             }
1079             view.findViewById(R.id.when_time).setVisibility(View.GONE);
1080 
1081         } else {
1082             // Show date for none all-day events
1083             whenDate = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flagsDate);
1084             String whenTime = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis,
1085                     flagsTime);
1086             if (repeatString != null) {
1087                 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")");
1088             } else {
1089                 setTextCommon(view, R.id.when_date, whenDate);
1090             }
1091 
1092             // Show the event timezone if it is different from the local timezone after the time
1093             String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater);
1094             if (!TextUtils.equals(localTimezone, eventTimezone)) {
1095                 String displayName;
1096                 // Figure out if this is in DST
1097                 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater));
1098                 if (allDay) {
1099                     date.timezone = Time.TIMEZONE_UTC;
1100                 }
1101                 date.set(mStartMillis);
1102 
1103                 TimeZone tz = TimeZone.getTimeZone(localTimezone);
1104                 if (tz == null || tz.getID().equals("GMT")) {
1105                     displayName = localTimezone;
1106                 } else {
1107                     displayName = tz.getDisplayName(date.isDst != 0, TimeZone.LONG);
1108                 }
1109                 setTextCommon(view, R.id.when_time, whenTime + " (" + displayName + ")");
1110             }
1111             else {
1112                 setTextCommon(view, R.id.when_time, whenTime);
1113             }
1114         }
1115 
1116 
1117         // Organizer view is setup in the updateCalendar method
1118 
1119 
1120         // Where
1121         if (location == null || location.trim().length() == 0) {
1122             setVisibilityCommon(view, R.id.where, View.GONE);
1123         } else {
1124             final TextView textView = mWhere;
1125             if (textView != null) {
1126                 textView.setAutoLinkMask(0);
1127                 textView.setText(location.trim());
1128                 linkifyTextView(textView);
1129 
1130                 textView.setOnTouchListener(new OnTouchListener() {
1131                     @Override
1132                     public boolean onTouch(View v, MotionEvent event) {
1133                         try {
1134                             return v.onTouchEvent(event);
1135                         } catch (ActivityNotFoundException e) {
1136                             // ignore
1137                             return true;
1138                         }
1139                     }
1140                 });
1141             }
1142         }
1143 
1144         // Description
1145         if (description != null && description.length() != 0) {
1146             setTextCommon(view, R.id.description, description);
1147         }
1148         updateDescription();  // Expand or collapse full description
1149     }
1150 
1151     /**
1152      * Replaces stretches of text that look like addresses and phone numbers with clickable
1153      * links.
1154      * <p>
1155      * This is really just an enhanced version of Linkify.addLinks().
1156      */
linkifyTextView(TextView textView)1157     private static void linkifyTextView(TextView textView) {
1158         /*
1159          * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
1160          * the current Linkify code will identify "94043" as a phone number and invite
1161          * you to dial it (and not provide a map link for the address).  We want to
1162          * have better recognition of phone numbers without losing any of the existing
1163          * annotations.
1164          *
1165          * Ideally this would be addressed by improving Linkify.  For now we manage it as
1166          * a second pass over the text.
1167          *
1168          * URIs and e-mail addresses are pretty easy to pick out of text.  Phone numbers
1169          * are a bit tricky because they have radically different formats in different
1170          * countries, in terms of both the digits and the way in which they are commonly
1171          * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
1172          * The expected format of a street address is defined in WebView.findAddress().  It's
1173          * pretty narrowly defined, so it won't often match.
1174          *
1175          * The RFC 3966 specification defines the format of a "tel:" URI.
1176          */
1177 
1178         /*
1179          * Start by letting Linkify find anything that isn't a phone number.  We have to let it
1180          * run first because every invocation removes all previous URLSpan annotations.
1181          */
1182         boolean linkifyFoundLinks = Linkify.addLinks(textView,
1183                 Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
1184 
1185         /*
1186          * Search for phone numbers.
1187          *
1188          * The "leniency" value can be VALID or POSSIBLE.  With VALID we won't match NANP numbers
1189          * shorter than 10 digits, which is inconvenient.  With POSSIBLE we get NANP 7-digit
1190          * numbers, and possibly strings of digits inside URIs, but happily we don't flag
1191          * five-digit zip codes like Linkify does.
1192          *
1193          * Phone links inside URIs will be annotated by the earlier URI linkification, so we just
1194          * need to avoid creating overlapping spans.
1195          */
1196         String defaultPhoneRegion = System.getProperty("user.region", "US");
1197         PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
1198         CharSequence text = textView.getText();
1199         Iterable<PhoneNumberMatch> phoneIterable = phoneUtil.findNumbers(text, defaultPhoneRegion,
1200                 PhoneNumberUtil.Leniency.POSSIBLE, Long.MAX_VALUE);
1201 
1202         /*
1203          * If the contents of the TextView are already Spannable (which will be the case if
1204          * Linkify found stuff, but might not be otherwise), we can just add annotations
1205          * to what's there.  If it's not, and we find phone numbers, we need to convert it to
1206          * a Spannable form.  (This mimics the behavior of Linkable.addLinks().)
1207          */
1208         Spannable spanText;
1209         if (text instanceof SpannableString) {
1210             spanText = (SpannableString) text;
1211         } else {
1212             spanText = SpannableString.valueOf(text);
1213         }
1214 
1215         /*
1216          * Get a list of any spans created by Linkify, for the overlapping span check.
1217          */
1218         URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1219 
1220         /*
1221          * Insert spans for the numbers we found.  We generate "tel:" URIs.
1222          */
1223         int phoneCount = 0;
1224         for (PhoneNumberMatch match : phoneIterable) {
1225             int start = match.start();
1226             int end = match.end();
1227 
1228             if (spanWillOverlap(spanText, existingSpans, start, end)) {
1229                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1230                     Log.v(TAG, "Not linkifying " + match.number().getNationalNumber() +
1231                             " as phone number due to overlap");
1232                 }
1233                 continue;
1234             }
1235 
1236             /*
1237              * A quick comparison of PhoneNumberUtil number parsing & formatting, with
1238              * defaultRegion="US":
1239              *
1240              * Input string     RFC3966                     NATIONAL
1241              * 5551212          +1-5551212                  555-1212
1242              * 6505551212       +1-650-555-1212             (650) 555-1212
1243              * 6505551212x123   +1-650-555-1212;ext=123     (650) 555-1212 ext. 123
1244              * +41446681800     +41-44-668-18-00            044 668 18 00
1245              *
1246              * The conversion of NANP 7-digit numbers to RFC3966 is not compatible with our dialer
1247              * (which tries to dial 8 digits, and fails).  So that won't work.
1248              *
1249              * The conversion of the Swiss number to NATIONAL format loses the country code,
1250              * so that won't work.
1251              *
1252              * The Linkify code takes the matching span and strips out everything that isn't a
1253              * digit or '+' sign.  We do the same here.  Extension numbers will get appended
1254              * without a separator, but the dialer wasn't doing anything useful with ";ext="
1255              * anyway.
1256              */
1257 
1258             //String dialStr = phoneUtil.format(match.number(),
1259             //        PhoneNumberUtil.PhoneNumberFormat.RFC3966);
1260             StringBuilder dialBuilder = new StringBuilder();
1261             for (int i = start; i < end; i++) {
1262                 char ch = spanText.charAt(i);
1263                 if (ch == '+' || Character.isDigit(ch)) {
1264                     dialBuilder.append(ch);
1265                 }
1266             }
1267             URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
1268 
1269             spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1270             phoneCount++;
1271         }
1272 
1273         if (phoneCount != 0) {
1274             // If we had to "upgrade" to Spannable, store the object into the TextView.
1275             if (spanText != text) {
1276                 textView.setText(spanText);
1277             }
1278 
1279             // Linkify.addLinks() sets the TextView movement method if it finds any links.  We
1280             // want to do the same here.  (This is cloned from Linkify.addLinkMovementMethod().)
1281             MovementMethod mm = textView.getMovementMethod();
1282 
1283             if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
1284                 if (textView.getLinksClickable()) {
1285                     textView.setMovementMethod(LinkMovementMethod.getInstance());
1286                 }
1287             }
1288         }
1289 
1290         if (!linkifyFoundLinks && phoneCount == 0) {
1291             if (Log.isLoggable(TAG, Log.VERBOSE)) {
1292                 Log.v(TAG, "No linkification matches, using geo default");
1293             }
1294             Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1295         }
1296     }
1297 
1298     /**
1299      * Determines whether a new span at [start,end) will overlap with any existing span.
1300      */
spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start, int end)1301     private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
1302             int end) {
1303         if (start == end) {
1304             // empty span, ignore
1305             return false;
1306         }
1307         for (URLSpan span : spanList) {
1308             int existingStart = spanText.getSpanStart(span);
1309             int existingEnd = spanText.getSpanEnd(span);
1310             if ((start >= existingStart && start < existingEnd) ||
1311                     end > existingStart && end <= existingEnd) {
1312                 return true;
1313             }
1314         }
1315 
1316         return false;
1317     }
1318 
sendAccessibilityEvent()1319     private void sendAccessibilityEvent() {
1320         AccessibilityManager am =
1321             (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
1322         if (!am.isEnabled()) {
1323             return;
1324         }
1325 
1326         AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1327         event.setClassName(getClass().getName());
1328         event.setPackageName(getActivity().getPackageName());
1329         List<CharSequence> text = event.getText();
1330 
1331         addFieldToAccessibilityEvent(text, mTitle);
1332         addFieldToAccessibilityEvent(text, mWhenDate);
1333         addFieldToAccessibilityEvent(text, mWhenTime);
1334         addFieldToAccessibilityEvent(text, mWhere);
1335         addFieldToAccessibilityEvent(text, mDesc);
1336 
1337         RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value);
1338         if (response.getVisibility() == View.VISIBLE) {
1339             int id = response.getCheckedRadioButtonId();
1340             if (id != View.NO_ID) {
1341                 text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
1342                 text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE));
1343             }
1344         }
1345 
1346         am.sendAccessibilityEvent(event);
1347     }
1348 
1349     /**
1350      * @param text
1351      */
addFieldToAccessibilityEvent(List<CharSequence> text, TextView view)1352     private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) {
1353         if (view == null) {
1354             return;
1355         }
1356         String str = view.getText().toString().trim();
1357         if (!TextUtils.isEmpty(str)) {
1358             text.add(str);
1359             text.add(PERIOD_SPACE);
1360         }
1361     }
1362 
updateCalendar(View view)1363     private void updateCalendar(View view) {
1364         mCalendarOwnerAccount = "";
1365         if (mCalendarsCursor != null && mEventCursor != null) {
1366             mCalendarsCursor.moveToFirst();
1367             String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1368             mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1369             mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1370 
1371             String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1372 
1373             // start duplicate calendars query
1374             mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI,
1375                     CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE,
1376                     new String[] {displayName}, null);
1377 
1378             String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1379             mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
1380             setTextCommon(view, R.id.organizer, eventOrganizer);
1381             if (!mIsOrganizer) {
1382                 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1383             } else {
1384                 setVisibilityCommon(view, R.id.organizer_container, View.GONE);
1385             }
1386             mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1387             mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL)
1388                     >= Calendars.CAL_ACCESS_CONTRIBUTOR;
1389             // TODO add "|| guestCanModify" after b/1299071 is fixed
1390             mCanModifyEvent = mCanModifyCalendar && mIsOrganizer;
1391             mIsBusyFreeCalendar =
1392                     mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY;
1393 
1394             if (!mIsBusyFreeCalendar) {
1395                 Button b = (Button) mView.findViewById(R.id.edit);
1396                 b.setEnabled(true);
1397                 b.setOnClickListener(new OnClickListener() {
1398                     @Override
1399                     public void onClick(View v) {
1400                         doEdit();
1401                         // For dialogs, just close the fragment
1402                         // For full screen, close activity on phone, leave it for tablet
1403                         if (mIsDialog) {
1404                             EventInfoFragment.this.dismiss();
1405                         }
1406                         else if (!mIsTabletConfig){
1407                             getActivity().finish();
1408                         }
1409                     }
1410                 });
1411             }
1412             View button;
1413             if (!mCanModifyCalendar) {
1414                 button = mView.findViewById(R.id.delete);
1415                 if (button != null) {
1416                     button.setEnabled(false);
1417                     button.setVisibility(View.GONE);
1418                 }
1419             }
1420             if (!mCanModifyEvent) {
1421                 button = mView.findViewById(R.id.edit);
1422                 if (button != null) {
1423                     button.setEnabled(false);
1424                     button.setVisibility(View.GONE);
1425                 }
1426             }
1427             if (!mIsTabletConfig && mMenu != null) {
1428                 mActivity.invalidateOptionsMenu();
1429             }
1430         } else {
1431             setVisibilityCommon(view, R.id.calendar, View.GONE);
1432             sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
1433         }
1434     }
1435 
1436     /**
1437      *
1438      */
updateMenu()1439     private void updateMenu() {
1440         if (mMenu == null) {
1441             return;
1442         }
1443         MenuItem delete = mMenu.findItem(R.id.info_action_delete);
1444         MenuItem edit = mMenu.findItem(R.id.info_action_edit);
1445         if (delete != null) {
1446             delete.setVisible(mCanModifyCalendar);
1447             delete.setEnabled(mCanModifyCalendar);
1448         }
1449         if (edit != null) {
1450             edit.setVisible(mCanModifyEvent);
1451             edit.setEnabled(mCanModifyEvent);
1452         }
1453     }
1454 
updateAttendees(View view)1455     private void updateAttendees(View view) {
1456         if (mAcceptedAttendees.size() + mDeclinedAttendees.size() +
1457                 mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) {
1458             mLongAttendees.clearAttendees();
1459             (mLongAttendees).addAttendees(mAcceptedAttendees);
1460             (mLongAttendees).addAttendees(mDeclinedAttendees);
1461             (mLongAttendees).addAttendees(mTentativeAttendees);
1462             (mLongAttendees).addAttendees(mNoResponseAttendees);
1463             mLongAttendees.setEnabled(false);
1464             mLongAttendees.setVisibility(View.VISIBLE);
1465         } else {
1466             mLongAttendees.setVisibility(View.GONE);
1467         }
1468     }
1469 
initReminders(View view, Cursor cursor)1470     public void initReminders(View view, Cursor cursor) {
1471 
1472         // Add reminders
1473         mOriginalReminders.clear();
1474         mUnsupportedReminders.clear();
1475         while (cursor.moveToNext()) {
1476             int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
1477             int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD);
1478 
1479             if (method != Reminders.METHOD_DEFAULT && !mReminderMethodValues.contains(method)) {
1480                 // Stash unsupported reminder types separately so we don't alter
1481                 // them in the UI
1482                 mUnsupportedReminders.add(ReminderEntry.valueOf(minutes, method));
1483             } else {
1484                 mOriginalReminders.add(ReminderEntry.valueOf(minutes, method));
1485             }
1486         }
1487         // Sort appropriately for display (by time, then type)
1488         Collections.sort(mOriginalReminders);
1489 
1490         if (mUserModifiedReminders) {
1491             // If the user has changed the list of reminders don't change what's
1492             // shown.
1493             return;
1494         }
1495 
1496         LinearLayout parent = (LinearLayout) mScrollView
1497                 .findViewById(R.id.reminder_items_container);
1498         if (parent != null) {
1499             parent.removeAllViews();
1500         }
1501         if (mReminderViews != null) {
1502             mReminderViews.clear();
1503         }
1504 
1505         if (mHasAlarm) {
1506             ArrayList<ReminderEntry> reminders = mOriginalReminders;
1507             // Insert any minute values that aren't represented in the minutes list.
1508             for (ReminderEntry re : reminders) {
1509                 EventViewUtils.addMinutesToList(
1510                         mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
1511             }
1512             // Create a UI element for each reminder.  We display all of the reminders we get
1513             // from the provider, even if the count exceeds the calendar maximum.  (Also, for
1514             // a new event, we won't have a maxReminders value available.)
1515             for (ReminderEntry re : reminders) {
1516                 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1517                         mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1518                         mReminderMethodLabels, re, Integer.MAX_VALUE, mReminderChangeListener);
1519             }
1520             // TODO show unsupported reminder types in some fashion.
1521         }
1522     }
1523 
formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type)1524     private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) {
1525         if (attendees.size() <= 0) {
1526             return;
1527         }
1528 
1529         int begin = sb.length();
1530         boolean firstTime = sb.length() == 0;
1531 
1532         if (firstTime == false) {
1533             begin += 2; // skip over the ", " for formatting.
1534         }
1535 
1536         for (Attendee attendee : attendees) {
1537             if (firstTime) {
1538                 firstTime = false;
1539             } else {
1540                 sb.append(", ");
1541             }
1542 
1543             String name = attendee.getDisplayName();
1544             sb.append(name);
1545         }
1546 
1547         switch (type) {
1548             case Attendees.ATTENDEE_STATUS_ACCEPTED:
1549                 break;
1550             case Attendees.ATTENDEE_STATUS_DECLINED:
1551                 sb.setSpan(new StrikethroughSpan(), begin, sb.length(),
1552                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1553                 // fall through
1554             default:
1555                 // The last INCLUSIVE causes the foreground color to be applied
1556                 // to the rest of the span. If not, the comma at the end of the
1557                 // declined or tentative may be black.
1558                 sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(),
1559                         Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
1560                 break;
1561         }
1562     }
1563 
updateResponse(View view)1564     void updateResponse(View view) {
1565         // we only let the user accept/reject/etc. a meeting if:
1566         // a) you can edit the event's containing calendar AND
1567         // b) you're not the organizer and only attendee AND
1568         // c) organizerCanRespond is enabled for the calendar
1569         // (if the attendee data has been hidden, the visible number of attendees
1570         // will be 1 -- the calendar owner's).
1571         // (there are more cases involved to be 100% accurate, such as
1572         // paying attention to whether or not an attendee status was
1573         // included in the feed, but we're currently omitting those corner cases
1574         // for simplicity).
1575 
1576         // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1577         if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1578                 (mIsOrganizer && !mOwnerCanRespond)) {
1579             setVisibilityCommon(view, R.id.response_container, View.GONE);
1580             return;
1581         }
1582 
1583         setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1584 
1585 
1586         int response;
1587         if (mUserSetResponse != CalendarController.ATTENDEE_NO_RESPONSE) {
1588             response = mUserSetResponse;
1589         } else if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) {
1590             response = mAttendeeResponseFromIntent;
1591         } else {
1592             response = mOriginalAttendeeResponse;
1593         }
1594 
1595         int buttonToCheck = findButtonIdForResponse(response);
1596         RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
1597         radioGroup.check(buttonToCheck); // -1 clear all radio buttons
1598         radioGroup.setOnCheckedChangeListener(this);
1599     }
1600 
setTextCommon(View view, int id, CharSequence text)1601     private void setTextCommon(View view, int id, CharSequence text) {
1602         TextView textView = (TextView) view.findViewById(id);
1603         if (textView == null)
1604             return;
1605         textView.setText(text);
1606     }
1607 
setVisibilityCommon(View view, int id, int visibility)1608     private void setVisibilityCommon(View view, int id, int visibility) {
1609         View v = view.findViewById(id);
1610         if (v != null) {
1611             v.setVisibility(visibility);
1612         }
1613         return;
1614     }
1615 
1616     /**
1617      * Taken from com.google.android.gm.HtmlConversationActivity
1618      *
1619      * Send the intent that shows the Contact info corresponding to the email address.
1620      */
showContactInfo(Attendee attendee, Rect rect)1621     public void showContactInfo(Attendee attendee, Rect rect) {
1622         // First perform lookup query to find existing contact
1623         final ContentResolver resolver = getActivity().getContentResolver();
1624         final String address = attendee.mEmail;
1625         final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1626                 Uri.encode(address));
1627         final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1628 
1629         if (lookupUri != null) {
1630             // Found matching contact, trigger QuickContact
1631             QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1632                     QuickContact.MODE_MEDIUM, null);
1633         } else {
1634             // No matching contact, ask user to create one
1635             final Uri mailUri = Uri.fromParts("mailto", address, null);
1636             final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1637 
1638             // Pass along full E-mail string for possible create dialog
1639             Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1640             intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
1641 
1642             // Only provide personal name hint if we have one
1643             final String senderPersonal = attendee.mName;
1644             if (!TextUtils.isEmpty(senderPersonal)) {
1645                 intent.putExtra(Intents.Insert.NAME, senderPersonal);
1646             }
1647 
1648             startActivity(intent);
1649         }
1650     }
1651 
1652     @Override
onPause()1653     public void onPause() {
1654         mIsPaused = true;
1655         mHandler.removeCallbacks(onDeleteRunnable);
1656         super.onPause();
1657     }
1658 
1659     @Override
onResume()1660     public void onResume() {
1661         super.onResume();
1662         mIsPaused = false;
1663         if (mDismissOnResume) {
1664             mHandler.post(onDeleteRunnable);
1665         }
1666     }
1667 
1668     @Override
eventsChanged()1669     public void eventsChanged() {
1670     }
1671 
1672     @Override
getSupportedEventTypes()1673     public long getSupportedEventTypes() {
1674         return EventType.EVENTS_CHANGED;
1675     }
1676 
1677     @Override
handleEvent(EventInfo event)1678     public void handleEvent(EventInfo event) {
1679         if (event.eventType == EventType.EVENTS_CHANGED && mHandler != null) {
1680             // reload the data
1681             mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
1682                     null, null, null);
1683         }
1684 
1685     }
1686 
1687 
1688     @Override
onClick(View view)1689     public void onClick(View view) {
1690 
1691         // This must be a click on one of the "remove reminder" buttons
1692         LinearLayout reminderItem = (LinearLayout) view.getParent();
1693         LinearLayout parent = (LinearLayout) reminderItem.getParent();
1694         parent.removeView(reminderItem);
1695         mReminderViews.remove(reminderItem);
1696         mUserModifiedReminders = true;
1697     }
1698 
1699 
1700     /**
1701      * Add a new reminder when the user hits the "add reminder" button.  We use the default
1702      * reminder time and method.
1703      */
addReminder()1704     private void addReminder() {
1705         // TODO: when adding a new reminder, make it different from the
1706         // last one in the list (if any).
1707         if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
1708             EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1709                     mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1710                     mReminderMethodLabels,
1711                     ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mMaxReminders,
1712                     mReminderChangeListener);
1713         } else {
1714             EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1715                     mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1716                     mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes),
1717                     mMaxReminders, mReminderChangeListener);
1718         }
1719     }
1720 
1721 
prepareReminders()1722     synchronized private void prepareReminders() {
1723         // Nothing to do if we've already built these lists _and_ we aren't
1724         // removing not allowed methods
1725         if (mReminderMinuteValues != null && mReminderMinuteLabels != null
1726                 && mReminderMethodValues != null && mReminderMethodLabels != null
1727                 && mCalendarAllowedReminders == null) {
1728             return;
1729         }
1730         // Load the labels and corresponding numeric values for the minutes and methods lists
1731         // from the assets.  If we're switching calendars, we need to clear and re-populate the
1732         // lists (which may have elements added and removed based on calendar properties).  This
1733         // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
1734         // new event that aren't in the default set.
1735         Resources r = mActivity.getResources();
1736         mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
1737         mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
1738         mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
1739         mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
1740 
1741         // Remove any reminder methods that aren't allowed for this calendar.  If this is
1742         // a new event, mCalendarAllowedReminders may not be set the first time we're called.
1743         Log.d(TAG, "AllowedReminders is " + mCalendarAllowedReminders);
1744         if (mCalendarAllowedReminders != null) {
1745             EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
1746                     mCalendarAllowedReminders);
1747         }
1748         if (mView != null) {
1749             mView.invalidate();
1750         }
1751     }
1752 
1753 
saveReminders()1754     private boolean saveReminders() {
1755         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
1756 
1757         // Read reminders from UI
1758         mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
1759                 mReminderMinuteValues, mReminderMethodValues);
1760         mOriginalReminders.addAll(mUnsupportedReminders);
1761         Collections.sort(mOriginalReminders);
1762         mReminders.addAll(mUnsupportedReminders);
1763         Collections.sort(mReminders);
1764 
1765         // Check if there are any changes in the reminder
1766         boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders,
1767                 mOriginalReminders, false /* no force save */);
1768 
1769         if (!changed) {
1770             return false;
1771         }
1772 
1773         // save new reminders
1774         AsyncQueryService service = new AsyncQueryService(getActivity());
1775         service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0);
1776         // Update the "hasAlarm" field for the event
1777         Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1778         int len = mReminders.size();
1779         boolean hasAlarm = len > 0;
1780         if (hasAlarm != mHasAlarm) {
1781             ContentValues values = new ContentValues();
1782             values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
1783             service.startUpdate(0, null, uri, values, null, null, 0);
1784         }
1785         return true;
1786     }
1787 
1788     /**
1789      * Loads an integer array asset into a list.
1790      */
loadIntegerArray(Resources r, int resNum)1791     private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
1792         int[] vals = r.getIntArray(resNum);
1793         int size = vals.length;
1794         ArrayList<Integer> list = new ArrayList<Integer>(size);
1795 
1796         for (int i = 0; i < size; i++) {
1797             list.add(vals[i]);
1798         }
1799 
1800         return list;
1801     }
1802     /**
1803      * Loads a String array asset into a list.
1804      */
loadStringArray(Resources r, int resNum)1805     private static ArrayList<String> loadStringArray(Resources r, int resNum) {
1806         String[] labels = r.getStringArray(resNum);
1807         ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
1808         return list;
1809     }
1810 
1811 }
1812