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_ALL_DAY;
20 import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
21 import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME;
22 import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.ObjectAnimator;
27 import android.app.Activity;
28 import android.app.Dialog;
29 import android.app.DialogFragment;
30 import android.app.FragmentManager;
31 import android.app.Service;
32 import android.content.ActivityNotFoundException;
33 import android.content.ContentProviderOperation;
34 import android.content.ContentResolver;
35 import android.content.ContentUris;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.content.DialogInterface;
39 import android.content.Intent;
40 import android.content.SharedPreferences;
41 import android.content.pm.ApplicationInfo;
42 import android.content.pm.PackageManager;
43 import android.content.pm.PackageManager.NameNotFoundException;
44 import android.content.res.Resources;
45 import android.database.Cursor;
46 import android.graphics.Color;
47 import android.graphics.Rect;
48 import android.graphics.drawable.Drawable;
49 import android.net.Uri;
50 import android.os.Bundle;
51 import android.provider.CalendarContract;
52 import android.provider.CalendarContract.Attendees;
53 import android.provider.CalendarContract.Calendars;
54 import android.provider.CalendarContract.Colors;
55 import android.provider.CalendarContract.Events;
56 import android.provider.CalendarContract.Reminders;
57 import android.provider.ContactsContract;
58 import android.provider.ContactsContract.CommonDataKinds;
59 import android.provider.ContactsContract.Intents;
60 import android.provider.ContactsContract.QuickContact;
61 import android.text.Spannable;
62 import android.text.SpannableStringBuilder;
63 import android.text.TextUtils;
64 import android.text.format.Time;
65 import android.text.method.LinkMovementMethod;
66 import android.text.method.MovementMethod;
67 import android.text.style.ForegroundColorSpan;
68 import android.text.util.Rfc822Token;
69 import android.util.Log;
70 import android.util.SparseIntArray;
71 import android.view.Gravity;
72 import android.view.LayoutInflater;
73 import android.view.Menu;
74 import android.view.MenuInflater;
75 import android.view.MenuItem;
76 import android.view.MotionEvent;
77 import android.view.View;
78 import android.view.View.OnClickListener;
79 import android.view.View.OnTouchListener;
80 import android.view.ViewGroup;
81 import android.view.Window;
82 import android.view.WindowManager;
83 import android.view.accessibility.AccessibilityEvent;
84 import android.view.accessibility.AccessibilityManager;
85 import android.widget.AdapterView;
86 import android.widget.AdapterView.OnItemSelectedListener;
87 import android.widget.Button;
88 import android.widget.LinearLayout;
89 import android.widget.RadioButton;
90 import android.widget.RadioGroup;
91 import android.widget.RadioGroup.OnCheckedChangeListener;
92 import android.widget.ScrollView;
93 import android.widget.TextView;
94 import android.widget.Toast;
95 
96 import com.android.calendar.CalendarController.EventInfo;
97 import com.android.calendar.CalendarController.EventType;
98 import com.android.calendar.CalendarEventModel.Attendee;
99 import com.android.calendar.CalendarEventModel.ReminderEntry;
100 import com.android.calendar.alerts.QuickResponseActivity;
101 import com.android.calendar.event.AttendeesView;
102 import com.android.calendar.event.EditEventActivity;
103 import com.android.calendar.event.EditEventHelper;
104 import com.android.calendar.event.EventColorPickerDialog;
105 import com.android.calendar.event.EventViewUtils;
106 import com.android.calendarcommon2.DateException;
107 import com.android.calendarcommon2.Duration;
108 import com.android.calendarcommon2.EventRecurrence;
109 import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
110 import com.android.colorpicker.HsvColorComparator;
111 
112 import java.util.ArrayList;
113 import java.util.Arrays;
114 import java.util.Collections;
115 import java.util.List;
116 
117 public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
118         CalendarController.EventHandler, OnClickListener, DeleteEventHelper.DeleteNotifyListener,
119         OnColorSelectedListener {
120 
121     public static final boolean DEBUG = false;
122 
123     public static final String TAG = "EventInfoFragment";
124     public static final String COLOR_PICKER_DIALOG_TAG = "EventColorPickerDialog";
125 
126     private static final int REQUEST_CODE_COLOR_PICKER = 0;
127 
128     protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
129     protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
130     protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
131     protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
132     protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible";
133     protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style";
134     protected static final String BUNDLE_KEY_CALENDAR_COLOR = "key_calendar_color";
135     protected static final String BUNDLE_KEY_CALENDAR_COLOR_INIT = "key_calendar_color_init";
136     protected static final String BUNDLE_KEY_CURRENT_COLOR = "key_current_color";
137     protected static final String BUNDLE_KEY_CURRENT_COLOR_KEY = "key_current_color_key";
138     protected static final String BUNDLE_KEY_CURRENT_COLOR_INIT = "key_current_color_init";
139     protected static final String BUNDLE_KEY_ORIGINAL_COLOR = "key_original_color";
140     protected static final String BUNDLE_KEY_ORIGINAL_COLOR_INIT = "key_original_color_init";
141     protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response";
142     protected static final String BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE =
143             "key_user_set_attendee_response";
144     protected static final String BUNDLE_KEY_TENTATIVE_USER_RESPONSE =
145             "key_tentative_user_response";
146     protected static final String BUNDLE_KEY_RESPONSE_WHICH_EVENTS = "key_response_which_events";
147     protected static final String BUNDLE_KEY_REMINDER_MINUTES = "key_reminder_minutes";
148     protected static final String BUNDLE_KEY_REMINDER_METHODS = "key_reminder_methods";
149 
150 
151     private static final String PERIOD_SPACE = ". ";
152 
153     private static final String NO_EVENT_COLOR = "";
154 
155     /**
156      * These are the corresponding indices into the array of strings
157      * "R.array.change_response_labels" in the resource file.
158      */
159     static final int UPDATE_SINGLE = 0;
160     static final int UPDATE_ALL = 1;
161 
162     // Style of view
163     public static final int FULL_WINDOW_STYLE = 0;
164     public static final int DIALOG_WINDOW_STYLE = 1;
165 
166     private int mWindowStyle = DIALOG_WINDOW_STYLE;
167 
168     // Query tokens for QueryHandler
169     private static final int TOKEN_QUERY_EVENT = 1 << 0;
170     private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
171     private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
172     private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
173     private static final int TOKEN_QUERY_REMINDERS = 1 << 4;
174     private static final int TOKEN_QUERY_VISIBLE_CALENDARS = 1 << 5;
175     private static final int TOKEN_QUERY_COLORS = 1 << 6;
176 
177     private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
178             | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT
179             | TOKEN_QUERY_REMINDERS | TOKEN_QUERY_VISIBLE_CALENDARS | TOKEN_QUERY_COLORS;
180 
181     private int mCurrentQuery = 0;
182 
183     private static final String[] EVENT_PROJECTION = new String[] {
184         Events._ID,                  // 0  do not remove; used in DeleteEventHelper
185         Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
186         Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
187         Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
188         Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
189         Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
190         Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
191         Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
192         Events.DESCRIPTION,          // 8
193         Events.EVENT_LOCATION,       // 9
194         Calendars.CALENDAR_ACCESS_LEVEL, // 10
195         Events.CALENDAR_COLOR,       // 11
196         Events.EVENT_COLOR,          // 12
197         Events.HAS_ATTENDEE_DATA,    // 13
198         Events.ORGANIZER,            // 14
199         Events.HAS_ALARM,            // 15
200         Calendars.MAX_REMINDERS,     // 16
201         Calendars.ALLOWED_REMINDERS, // 17
202         Events.CUSTOM_APP_PACKAGE,   // 18
203         Events.CUSTOM_APP_URI,       // 19
204         Events.DTEND,                // 20
205         Events.DURATION,             // 21
206         Events.ORIGINAL_SYNC_ID      // 22 do not remove; used in DeleteEventHelper
207     };
208     private static final int EVENT_INDEX_ID = 0;
209     private static final int EVENT_INDEX_TITLE = 1;
210     private static final int EVENT_INDEX_RRULE = 2;
211     private static final int EVENT_INDEX_ALL_DAY = 3;
212     private static final int EVENT_INDEX_CALENDAR_ID = 4;
213     private static final int EVENT_INDEX_DTSTART = 5;
214     private static final int EVENT_INDEX_SYNC_ID = 6;
215     private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
216     private static final int EVENT_INDEX_DESCRIPTION = 8;
217     private static final int EVENT_INDEX_EVENT_LOCATION = 9;
218     private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
219     private static final int EVENT_INDEX_CALENDAR_COLOR = 11;
220     private static final int EVENT_INDEX_EVENT_COLOR = 12;
221     private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 13;
222     private static final int EVENT_INDEX_ORGANIZER = 14;
223     private static final int EVENT_INDEX_HAS_ALARM = 15;
224     private static final int EVENT_INDEX_MAX_REMINDERS = 16;
225     private static final int EVENT_INDEX_ALLOWED_REMINDERS = 17;
226     private static final int EVENT_INDEX_CUSTOM_APP_PACKAGE = 18;
227     private static final int EVENT_INDEX_CUSTOM_APP_URI = 19;
228     private static final int EVENT_INDEX_DTEND = 20;
229     private static final int EVENT_INDEX_DURATION = 21;
230 
231     private static final String[] ATTENDEES_PROJECTION = new String[] {
232         Attendees._ID,                      // 0
233         Attendees.ATTENDEE_NAME,            // 1
234         Attendees.ATTENDEE_EMAIL,           // 2
235         Attendees.ATTENDEE_RELATIONSHIP,    // 3
236         Attendees.ATTENDEE_STATUS,          // 4
237         Attendees.ATTENDEE_IDENTITY,        // 5
238         Attendees.ATTENDEE_ID_NAMESPACE     // 6
239     };
240     private static final int ATTENDEES_INDEX_ID = 0;
241     private static final int ATTENDEES_INDEX_NAME = 1;
242     private static final int ATTENDEES_INDEX_EMAIL = 2;
243     private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
244     private static final int ATTENDEES_INDEX_STATUS = 4;
245     private static final int ATTENDEES_INDEX_IDENTITY = 5;
246     private static final int ATTENDEES_INDEX_ID_NAMESPACE = 6;
247 
248     static {
249         if (!Utils.isJellybeanOrLater()) {
250             EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID; // dummy value
251             EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID; // dummy value
252 
253             ATTENDEES_PROJECTION[ATTENDEES_INDEX_IDENTITY] = Attendees._ID; // dummy value
254             ATTENDEES_PROJECTION[ATTENDEES_INDEX_ID_NAMESPACE] = Attendees._ID; // dummy value
255         }
256     }
257 
258     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
259 
260     private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
261             + Attendees.ATTENDEE_EMAIL + " ASC";
262 
263     private static final String[] REMINDERS_PROJECTION = new String[] {
264         Reminders._ID,                      // 0
265         Reminders.MINUTES,            // 1
266         Reminders.METHOD           // 2
267     };
268     private static final int REMINDERS_INDEX_ID = 0;
269     private static final int REMINDERS_MINUTES_ID = 1;
270     private static final int REMINDERS_METHOD_ID = 2;
271 
272     private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
273 
274     static final String[] CALENDARS_PROJECTION = new String[] {
275         Calendars._ID,           // 0
276         Calendars.CALENDAR_DISPLAY_NAME,  // 1
277         Calendars.OWNER_ACCOUNT, // 2
278         Calendars.CAN_ORGANIZER_RESPOND, // 3
279         Calendars.ACCOUNT_NAME, // 4
280         Calendars.ACCOUNT_TYPE  // 5
281     };
282     static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
283     static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
284     static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
285     static final int CALENDARS_INDEX_ACCOUNT_NAME = 4;
286     static final int CALENDARS_INDEX_ACCOUNT_TYPE = 5;
287 
288     static final String CALENDARS_WHERE = Calendars._ID + "=?";
289     static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
290     static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?";
291 
292     static final String[] COLORS_PROJECTION = new String[] {
293         Colors._ID, // 0
294         Colors.COLOR, // 1
295         Colors.COLOR_KEY // 2
296     };
297 
298     static final String COLORS_WHERE = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE +
299         "=? AND " + Colors.COLOR_TYPE + "=" + Colors.TYPE_EVENT;
300 
301     public static final int COLORS_INDEX_COLOR = 1;
302     public static final int COLORS_INDEX_COLOR_KEY = 2;
303 
304     private View mView;
305 
306     private Uri mUri;
307     private long mEventId;
308     private Cursor mEventCursor;
309     private Cursor mAttendeesCursor;
310     private Cursor mCalendarsCursor;
311     private Cursor mRemindersCursor;
312 
313     private static float mScale = 0; // Used for supporting different screen densities
314 
315     private static int mCustomAppIconSize = 32;
316 
317     private long mStartMillis;
318     private long mEndMillis;
319     private boolean mAllDay;
320 
321     private boolean mHasAttendeeData;
322     private String mEventOrganizerEmail;
323     private String mEventOrganizerDisplayName = "";
324     private boolean mIsOrganizer;
325     private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
326     private boolean mOwnerCanRespond;
327     private String mSyncAccountName;
328     private String mCalendarOwnerAccount;
329     private boolean mCanModifyCalendar;
330     private boolean mCanModifyEvent;
331     private boolean mIsBusyFreeCalendar;
332     private int mNumOfAttendees;
333     private EditResponseHelper mEditResponseHelper;
334     private boolean mDeleteDialogVisible = false;
335     private DeleteEventHelper mDeleteHelper;
336 
337     private int mOriginalAttendeeResponse;
338     private int mAttendeeResponseFromIntent = Attendees.ATTENDEE_STATUS_NONE;
339     private int mUserSetResponse = Attendees.ATTENDEE_STATUS_NONE;
340     private int mWhichEvents = -1;
341     // Used as the temporary response until the dialog is confirmed. It is also
342     // able to be used as a state marker for configuration changes.
343     private int mTentativeUserSetResponse = Attendees.ATTENDEE_STATUS_NONE;
344     private boolean mIsRepeating;
345     private boolean mHasAlarm;
346     private int mMaxReminders;
347     private String mCalendarAllowedReminders;
348     // Used to prevent saving changes in event if it is being deleted.
349     private boolean mEventDeletionStarted = false;
350 
351     private TextView mTitle;
352     private TextView mWhenDateTime;
353     private TextView mWhere;
354     private ExpandableTextView mDesc;
355     private AttendeesView mLongAttendees;
356     private Button emailAttendeesButton;
357     private Menu mMenu = null;
358     private View mHeadlines;
359     private ScrollView mScrollView;
360     private View mLoadingMsgView;
361     private View mErrorMsgView;
362     private ObjectAnimator mAnimateAlpha;
363     private long mLoadingMsgStartTime;
364 
365     private EventColorPickerDialog mColorPickerDialog;
366     private SparseIntArray mDisplayColorKeyMap = new SparseIntArray();
367     private int[] mColors;
368     private int mOriginalColor = -1;
369     private boolean mOriginalColorInitialized = false;
370     private int mCalendarColor = -1;
371     private boolean mCalendarColorInitialized = false;
372     private int mCurrentColor = -1;
373     private boolean mCurrentColorInitialized = false;
374     private int mCurrentColorKey = -1;
375 
376     private static final int FADE_IN_TIME = 300;   // in milliseconds
377     private static final int LOADING_MSG_DELAY = 600;   // in milliseconds
378     private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600;
379     private boolean mNoCrossFade = false;  // Used to prevent repeated cross-fade
380     private RadioGroup mResponseRadioGroup;
381 
382     ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
383     ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
384     ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
385     ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
386     ArrayList<String> mToEmails = new ArrayList<String>();
387     ArrayList<String> mCcEmails = new ArrayList<String>();
388 
389     private int mDefaultReminderMinutes;
390     private final ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0);
391     public ArrayList<ReminderEntry> mReminders;
392     public ArrayList<ReminderEntry> mOriginalReminders = new ArrayList<ReminderEntry>();
393     public ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>();
394     private boolean mUserModifiedReminders = false;
395 
396     /**
397      * Contents of the "minutes" spinner.  This has default values from the XML file, augmented
398      * with any additional values that were already associated with the event.
399      */
400     private ArrayList<Integer> mReminderMinuteValues;
401     private ArrayList<String> mReminderMinuteLabels;
402 
403     /**
404      * Contents of the "methods" spinner.  The "values" list specifies the method constant
405      * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels.  Any methods that
406      * aren't allowed by the Calendar will be removed.
407      */
408     private ArrayList<Integer> mReminderMethodValues;
409     private ArrayList<String> mReminderMethodLabels;
410 
411     private QueryHandler mHandler;
412 
413 
414     private final Runnable mTZUpdater = new Runnable() {
415         @Override
416         public void run() {
417             updateEvent(mView);
418         }
419     };
420 
421     private final Runnable mLoadingMsgAlphaUpdater = new Runnable() {
422         @Override
423         public void run() {
424             // Since this is run after a delay, make sure to only show the message
425             // if the event's data is not shown yet.
426             if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) {
427                 mLoadingMsgStartTime = System.currentTimeMillis();
428                 mLoadingMsgView.setAlpha(1);
429             }
430         }
431     };
432 
433     private OnItemSelectedListener mReminderChangeListener;
434 
435     private static int mDialogWidth = 500;
436     private static int mDialogHeight = 600;
437     private static int DIALOG_TOP_MARGIN = 8;
438     private boolean mIsDialog = false;
439     private boolean mIsPaused = true;
440     private boolean mDismissOnResume = false;
441     private int mX = -1;
442     private int mY = -1;
443     private int mMinTop;         // Dialog cannot be above this location
444     private boolean mIsTabletConfig;
445     private Activity mActivity;
446     private Context mContext;
447 
448     private CalendarController mController;
449 
450     private class QueryHandler extends AsyncQueryService {
QueryHandler(Context context)451         public QueryHandler(Context context) {
452             super(context);
453         }
454 
455         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)456         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
457             // if the activity is finishing, then close the cursor and return
458             final Activity activity = getActivity();
459             if (activity == null || activity.isFinishing()) {
460                 if (cursor != null) {
461                     cursor.close();
462                 }
463                 return;
464             }
465 
466             switch (token) {
467             case TOKEN_QUERY_EVENT:
468                 mEventCursor = Utils.matrixCursorFromCursor(cursor);
469                 if (!initEventCursor()) {
470                     displayEventNotFound();
471                     return;
472                 }
473                 if (!mCalendarColorInitialized) {
474                     mCalendarColor = Utils.getDisplayColorFromColor(
475                             mEventCursor.getInt(EVENT_INDEX_CALENDAR_COLOR));
476                     mCalendarColorInitialized = true;
477                 }
478 
479                 if (!mOriginalColorInitialized) {
480                     mOriginalColor = mEventCursor.isNull(EVENT_INDEX_EVENT_COLOR)
481                             ? mCalendarColor : Utils.getDisplayColorFromColor(
482                                     mEventCursor.getInt(EVENT_INDEX_EVENT_COLOR));
483                     mOriginalColorInitialized = true;
484                 }
485 
486                 if (!mCurrentColorInitialized) {
487                     mCurrentColor = mOriginalColor;
488                     mCurrentColorInitialized = true;
489                 }
490 
491                 updateEvent(mView);
492                 prepareReminders();
493 
494                 // start calendar query
495                 Uri uri = Calendars.CONTENT_URI;
496                 String[] args = new String[] {
497                         Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
498                 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
499                         CALENDARS_WHERE, args, null);
500                 break;
501             case TOKEN_QUERY_CALENDARS:
502                 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
503                 updateCalendar(mView);
504                 // FRAG_TODO fragments shouldn't set the title anymore
505                 updateTitle();
506 
507                 args = new String[] {
508                         mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME),
509                         mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_TYPE) };
510                 uri = Colors.CONTENT_URI;
511                 startQuery(TOKEN_QUERY_COLORS, null, uri, COLORS_PROJECTION, COLORS_WHERE, args,
512                         null);
513 
514                 if (!mIsBusyFreeCalendar) {
515                     args = new String[] { Long.toString(mEventId) };
516 
517                     // start attendees query
518                     uri = Attendees.CONTENT_URI;
519                     startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
520                             ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
521                 } else {
522                     sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
523                 }
524                 if (mHasAlarm) {
525                     // start reminders query
526                     args = new String[] { Long.toString(mEventId) };
527                     uri = Reminders.CONTENT_URI;
528                     startQuery(TOKEN_QUERY_REMINDERS, null, uri,
529                             REMINDERS_PROJECTION, REMINDERS_WHERE, args, null);
530                 } else {
531                     sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS);
532                 }
533                 break;
534             case TOKEN_QUERY_COLORS:
535                 ArrayList<Integer> colors = new ArrayList<Integer>();
536                 if (cursor.moveToFirst()) {
537                     do
538                     {
539                         int colorKey = cursor.getInt(COLORS_INDEX_COLOR_KEY);
540                         int rawColor = cursor.getInt(COLORS_INDEX_COLOR);
541                         int displayColor = Utils.getDisplayColorFromColor(rawColor);
542                         mDisplayColorKeyMap.put(displayColor, colorKey);
543                         colors.add(displayColor);
544                     } while (cursor.moveToNext());
545                 }
546                 cursor.close();
547                 Integer[] sortedColors = new Integer[colors.size()];
548                 Arrays.sort(colors.toArray(sortedColors), new HsvColorComparator());
549                 mColors = new int[sortedColors.length];
550                 for (int i = 0; i < sortedColors.length; i++) {
551                     mColors[i] = sortedColors[i].intValue();
552 
553                     float[] hsv = new float[3];
554                     Color.colorToHSV(mColors[i], hsv);
555                     if (DEBUG) {
556                         Log.d("Color", "H:" + hsv[0] + ",S:" + hsv[1] + ",V:" + hsv[2]);
557                     }
558                 }
559                 if (mCanModifyCalendar) {
560                     View button = mView.findViewById(R.id.change_color);
561                     if (button != null && mColors.length > 0) {
562                         button.setEnabled(true);
563                         button.setVisibility(View.VISIBLE);
564                     }
565                 }
566                 updateMenu();
567                 break;
568             case TOKEN_QUERY_ATTENDEES:
569                 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
570                 initAttendeesCursor(mView);
571                 updateResponse(mView);
572                 break;
573             case TOKEN_QUERY_REMINDERS:
574                 mRemindersCursor = Utils.matrixCursorFromCursor(cursor);
575                 initReminders(mView, mRemindersCursor);
576                 break;
577             case TOKEN_QUERY_VISIBLE_CALENDARS:
578                 if (cursor.getCount() > 1) {
579                     // Start duplicate calendars query to detect whether to add the calendar
580                     // email to the calendar owner display.
581                     String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
582                     mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null,
583                             Calendars.CONTENT_URI, CALENDARS_PROJECTION,
584                             CALENDARS_DUPLICATE_NAME_WHERE, new String[] {displayName}, null);
585                 } else {
586                     // Don't need to display the calendar owner when there is only a single
587                     // calendar.  Skip the duplicate calendars query.
588                     setVisibilityCommon(mView, R.id.calendar_container, View.GONE);
589                     mCurrentQuery |= TOKEN_QUERY_DUPLICATE_CALENDARS;
590                 }
591                 break;
592             case TOKEN_QUERY_DUPLICATE_CALENDARS:
593                 SpannableStringBuilder sb = new SpannableStringBuilder();
594 
595                 // Calendar display name
596                 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
597                 sb.append(calendarName);
598 
599                 // Show email account if display name is not unique and
600                 // display name != email
601                 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
602                 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) &&
603                         Utils.isValidEmail(email)) {
604                     sb.append(" (").append(email).append(")");
605                 }
606 
607                 setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE);
608                 setTextCommon(mView, R.id.calendar_name, sb);
609                 break;
610             }
611             cursor.close();
612             sendAccessibilityEventIfQueryDone(token);
613 
614             // All queries are done, show the view.
615             if (mCurrentQuery == TOKEN_QUERY_ALL) {
616                 if (mLoadingMsgView.getAlpha() == 1) {
617                     // Loading message is showing, let it stay a bit more (to prevent
618                     // flashing) by adding a start delay to the event animation
619                     long timeDiff = LOADING_MSG_MIN_DISPLAY_TIME - (System.currentTimeMillis() -
620                             mLoadingMsgStartTime);
621                     if (timeDiff > 0) {
622                         mAnimateAlpha.setStartDelay(timeDiff);
623                     }
624                 }
625                 if (!mAnimateAlpha.isRunning() &&!mAnimateAlpha.isStarted() && !mNoCrossFade) {
626                     mAnimateAlpha.start();
627                 } else {
628                     mScrollView.setAlpha(1);
629                     mLoadingMsgView.setVisibility(View.GONE);
630                 }
631             }
632         }
633     }
634 
sendAccessibilityEventIfQueryDone(int token)635     private void sendAccessibilityEventIfQueryDone(int token) {
636         mCurrentQuery |= token;
637         if (mCurrentQuery == TOKEN_QUERY_ALL) {
638             sendAccessibilityEvent();
639         }
640     }
641 
EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle, ArrayList<ReminderEntry> reminders)642     public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
643             int attendeeResponse, boolean isDialog, int windowStyle,
644             ArrayList<ReminderEntry> reminders) {
645 
646         Resources r = context.getResources();
647         if (mScale == 0) {
648             mScale = context.getResources().getDisplayMetrics().density;
649             if (mScale != 1) {
650                 mCustomAppIconSize *= mScale;
651                 if (isDialog) {
652                     DIALOG_TOP_MARGIN *= mScale;
653                 }
654             }
655         }
656         if (isDialog) {
657             setDialogSize(r);
658         }
659         mIsDialog = isDialog;
660 
661         setStyle(DialogFragment.STYLE_NO_TITLE, 0);
662         mUri = uri;
663         mStartMillis = startMillis;
664         mEndMillis = endMillis;
665         mAttendeeResponseFromIntent = attendeeResponse;
666         mWindowStyle = windowStyle;
667 
668         // Pass in null if no reminders are being specified.
669         // This may be used to explicitly show certain reminders already known
670         // about, such as during configuration changes.
671         mReminders = reminders;
672     }
673 
674     // This is currently required by the fragment manager.
EventInfoFragment()675     public EventInfoFragment() {
676     }
677 
EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle, ArrayList<ReminderEntry> reminders)678     public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
679             int attendeeResponse, boolean isDialog, int windowStyle,
680             ArrayList<ReminderEntry> reminders) {
681         this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
682                 endMillis, attendeeResponse, isDialog, windowStyle, reminders);
683         mEventId = eventId;
684     }
685 
686     @Override
onActivityCreated(Bundle savedInstanceState)687     public void onActivityCreated(Bundle savedInstanceState) {
688         super.onActivityCreated(savedInstanceState);
689 
690         mReminderChangeListener = new OnItemSelectedListener() {
691             @Override
692             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
693                 Integer prevValue = (Integer) parent.getTag();
694                 if (prevValue == null || prevValue != position) {
695                     parent.setTag(position);
696                     mUserModifiedReminders = true;
697                 }
698             }
699 
700             @Override
701             public void onNothingSelected(AdapterView<?> parent) {
702                 // do nothing
703             }
704 
705         };
706 
707         if (savedInstanceState != null) {
708             mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
709             mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
710                     DIALOG_WINDOW_STYLE);
711         }
712 
713         if (mIsDialog) {
714             applyDialogParams();
715         }
716 
717         final Activity activity = getActivity();
718         mContext = activity;
719         mColorPickerDialog = (EventColorPickerDialog) activity.getFragmentManager()
720                 .findFragmentByTag(COLOR_PICKER_DIALOG_TAG);
721         if (mColorPickerDialog != null) {
722             mColorPickerDialog.setOnColorSelectedListener(this);
723         }
724     }
725 
applyDialogParams()726     private void applyDialogParams() {
727         Dialog dialog = getDialog();
728         dialog.setCanceledOnTouchOutside(true);
729 
730         Window window = dialog.getWindow();
731         window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
732 
733         WindowManager.LayoutParams a = window.getAttributes();
734         a.dimAmount = .4f;
735 
736         a.width = mDialogWidth;
737         a.height = mDialogHeight;
738 
739 
740         // On tablets , do smart positioning of dialog
741         // On phones , use the whole screen
742 
743         if (mX != -1 || mY != -1) {
744             a.x = mX - mDialogWidth / 2;
745             a.y = mY - mDialogHeight / 2;
746             if (a.y < mMinTop) {
747                 a.y = mMinTop + DIALOG_TOP_MARGIN;
748             }
749             a.gravity = Gravity.LEFT | Gravity.TOP;
750         }
751         window.setAttributes(a);
752     }
753 
setDialogParams(int x, int y, int minTop)754     public void setDialogParams(int x, int y, int minTop) {
755         mX = x;
756         mY = y;
757         mMinTop = minTop;
758     }
759 
760     // Implements OnCheckedChangeListener
761     @Override
onCheckedChanged(RadioGroup group, int checkedId)762     public void onCheckedChanged(RadioGroup group, int checkedId) {
763         // If we haven't finished the return from the dialog yet, don't display.
764         if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
765             return;
766         }
767 
768         // If this is not a repeating event, then don't display the dialog
769         // asking which events to change.
770         int response = getResponseFromButtonId(checkedId);
771         if (!mIsRepeating) {
772             mUserSetResponse = response;
773             return;
774         }
775 
776         // If the selection is the same as the original, then don't display the
777         // dialog asking which events to change.
778         if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
779             mUserSetResponse = response;
780             return;
781         }
782 
783         // This is a repeating event. We need to ask the user if they mean to
784         // change just this one instance or all instances.
785         mTentativeUserSetResponse = response;
786         mEditResponseHelper.showDialog(mWhichEvents);
787     }
788 
onNothingSelected(AdapterView<?> parent)789     public void onNothingSelected(AdapterView<?> parent) {
790     }
791 
792     @Override
onDetach()793     public void onDetach() {
794         super.onDetach();
795         mController.deregisterEventHandler(R.layout.event_info);
796     }
797 
798     @Override
onAttach(Activity activity)799     public void onAttach(Activity activity) {
800         super.onAttach(activity);
801         mActivity = activity;
802         // Ensure that mIsTabletConfig is set before creating the menu.
803         mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config);
804         mController = CalendarController.getInstance(mActivity);
805         mController.registerEventHandler(R.layout.event_info, this);
806         mEditResponseHelper = new EditResponseHelper(activity);
807         mEditResponseHelper.setDismissListener(
808                 new DialogInterface.OnDismissListener() {
809             @Override
810             public void onDismiss(DialogInterface dialog) {
811                 // If the user dismisses the dialog (without hitting OK),
812                 // then we want to revert the selection that opened the dialog.
813                 if (mEditResponseHelper.getWhichEvents() != -1) {
814                     mUserSetResponse = mTentativeUserSetResponse;
815                     mWhichEvents = mEditResponseHelper.getWhichEvents();
816                 } else {
817                     // Revert the attending response radio selection to whatever
818                     // was selected prior to this selection (possibly nothing).
819                     int oldResponse;
820                     if (mUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
821                         oldResponse = mUserSetResponse;
822                     } else {
823                         oldResponse = mOriginalAttendeeResponse;
824                     }
825                     int buttonToCheck = findButtonIdForResponse(oldResponse);
826 
827                     if (mResponseRadioGroup != null) {
828                         mResponseRadioGroup.check(buttonToCheck);
829                     }
830 
831                     // If the radio group is being cleared, also clear the
832                     // dialog's selection of which events should be included
833                     // in this response.
834                     if (buttonToCheck == -1) {
835                         mEditResponseHelper.setWhichEvents(-1);
836                     }
837                 }
838 
839                 // Since OnPause will force the dialog to dismiss, do
840                 // not change the dialog status
841                 if (!mIsPaused) {
842                     mTentativeUserSetResponse = Attendees.ATTENDEE_STATUS_NONE;
843                 }
844             }
845         });
846 
847         if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
848             mEditResponseHelper.setWhichEvents(UPDATE_ALL);
849             mWhichEvents = mEditResponseHelper.getWhichEvents();
850         }
851         mHandler = new QueryHandler(activity);
852         if (!mIsDialog) {
853             setHasOptionsMenu(true);
854         }
855     }
856 
857     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)858     public View onCreateView(LayoutInflater inflater, ViewGroup container,
859             Bundle savedInstanceState) {
860 
861         if (savedInstanceState != null) {
862             mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
863             mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
864                     DIALOG_WINDOW_STYLE);
865             mDeleteDialogVisible =
866                 savedInstanceState.getBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE,false);
867             mCalendarColor = savedInstanceState.getInt(BUNDLE_KEY_CALENDAR_COLOR);
868             mCalendarColorInitialized =
869                     savedInstanceState.getBoolean(BUNDLE_KEY_CALENDAR_COLOR_INIT);
870             mOriginalColor = savedInstanceState.getInt(BUNDLE_KEY_ORIGINAL_COLOR);
871             mOriginalColorInitialized = savedInstanceState.getBoolean(
872                     BUNDLE_KEY_ORIGINAL_COLOR_INIT);
873             mCurrentColor = savedInstanceState.getInt(BUNDLE_KEY_CURRENT_COLOR);
874             mCurrentColorInitialized = savedInstanceState.getBoolean(
875                     BUNDLE_KEY_CURRENT_COLOR_INIT);
876             mCurrentColorKey = savedInstanceState.getInt(BUNDLE_KEY_CURRENT_COLOR_KEY);
877 
878             mTentativeUserSetResponse = savedInstanceState.getInt(
879                             BUNDLE_KEY_TENTATIVE_USER_RESPONSE,
880                             Attendees.ATTENDEE_STATUS_NONE);
881             if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE &&
882                     mEditResponseHelper != null) {
883                 // If the edit response helper dialog is open, we'll need to
884                 // know if either of the choices were selected.
885                 mEditResponseHelper.setWhichEvents(savedInstanceState.getInt(
886                         BUNDLE_KEY_RESPONSE_WHICH_EVENTS, -1));
887             }
888             mUserSetResponse = savedInstanceState.getInt(
889                     BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE,
890                     Attendees.ATTENDEE_STATUS_NONE);
891             if (mUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
892                 // If the response was set by the user before a configuration
893                 // change, we'll need to know which choice was selected.
894                 mWhichEvents = savedInstanceState.getInt(
895                         BUNDLE_KEY_RESPONSE_WHICH_EVENTS, -1);
896             }
897 
898             mReminders = Utils.readRemindersFromBundle(savedInstanceState);
899         }
900 
901         if (mWindowStyle == DIALOG_WINDOW_STYLE) {
902             mView = inflater.inflate(R.layout.event_info_dialog, container, false);
903         } else {
904             mView = inflater.inflate(R.layout.event_info, container, false);
905         }
906         mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view);
907         mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg);
908         mErrorMsgView = mView.findViewById(R.id.event_info_error_msg);
909         mTitle = (TextView) mView.findViewById(R.id.title);
910         mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime);
911         mWhere = (TextView) mView.findViewById(R.id.where);
912         mDesc = (ExpandableTextView) mView.findViewById(R.id.description);
913         mHeadlines = mView.findViewById(R.id.event_info_headline);
914         mLongAttendees = (AttendeesView) mView.findViewById(R.id.long_attendee_list);
915 
916         mResponseRadioGroup = (RadioGroup) mView.findViewById(R.id.response_value);
917 
918         if (mUri == null) {
919             // restore event ID from bundle
920             mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
921             mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
922             mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
923             mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
924         }
925 
926         mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1);
927         mAnimateAlpha.setDuration(FADE_IN_TIME);
928         mAnimateAlpha.addListener(new AnimatorListenerAdapter() {
929             int defLayerType;
930 
931             @Override
932             public void onAnimationStart(Animator animation) {
933                 // Use hardware layer for better performance during animation
934                 defLayerType = mScrollView.getLayerType();
935                 mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
936                 // Ensure that the loading message is gone before showing the
937                 // event info
938                 mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater);
939                 mLoadingMsgView.setVisibility(View.GONE);
940             }
941 
942             @Override
943             public void onAnimationCancel(Animator animation) {
944                 mScrollView.setLayerType(defLayerType, null);
945             }
946 
947             @Override
948             public void onAnimationEnd(Animator animation) {
949                 mScrollView.setLayerType(defLayerType, null);
950                 // Do not cross fade after the first time
951                 mNoCrossFade = true;
952             }
953         });
954 
955         mLoadingMsgView.setAlpha(0);
956         mScrollView.setAlpha(0);
957         mErrorMsgView.setVisibility(View.INVISIBLE);
958         mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY);
959 
960         // start loading the data
961 
962         mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
963                 null, null, null);
964 
965         View b = mView.findViewById(R.id.delete);
966         b.setOnClickListener(new OnClickListener() {
967             @Override
968             public void onClick(View v) {
969                 if (!mCanModifyCalendar) {
970                     return;
971                 }
972                 mDeleteHelper =
973                         new DeleteEventHelper(mContext, mActivity, !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
974                 mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
975                 mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
976                 mDeleteDialogVisible = true;
977                 mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
978             }
979         });
980 
981         b = mView.findViewById(R.id.change_color);
982         b.setOnClickListener(new OnClickListener() {
983             @Override
984             public void onClick(View v) {
985                 if (!mCanModifyCalendar) {
986                     return;
987                 }
988                 showEventColorPickerDialog();
989             }
990         });
991 
992         // Hide Edit/Delete buttons if in full screen mode on a phone
993         if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
994             mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE);
995         }
996 
997         // Create a listener for the email guests button
998         emailAttendeesButton = (Button) mView.findViewById(R.id.email_attendees_button);
999         if (emailAttendeesButton != null) {
1000             emailAttendeesButton.setOnClickListener(new View.OnClickListener() {
1001                 @Override
1002                 public void onClick(View v) {
1003                     emailAttendees();
1004                 }
1005             });
1006         }
1007 
1008         // Create a listener for the add reminder button
1009         View reminderAddButton = mView.findViewById(R.id.reminder_add);
1010         View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
1011             @Override
1012             public void onClick(View v) {
1013                 addReminder();
1014                 mUserModifiedReminders = true;
1015             }
1016         };
1017         reminderAddButton.setOnClickListener(addReminderOnClickListener);
1018 
1019         // Set reminders variables
1020 
1021         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
1022         String defaultReminderString = prefs.getString(
1023                 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
1024         mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
1025         prepareReminders();
1026 
1027         return mView;
1028     }
1029 
1030     private final Runnable onDeleteRunnable = new Runnable() {
1031         @Override
1032         public void run() {
1033             if (EventInfoFragment.this.mIsPaused) {
1034                 mDismissOnResume = true;
1035                 return;
1036             }
1037             if (EventInfoFragment.this.isVisible()) {
1038                 EventInfoFragment.this.dismiss();
1039             }
1040         }
1041     };
1042 
updateTitle()1043     private void updateTitle() {
1044         Resources res = getActivity().getResources();
1045         if (mCanModifyCalendar && !mIsOrganizer) {
1046             getActivity().setTitle(res.getString(R.string.event_info_title_invite));
1047         } else {
1048             getActivity().setTitle(res.getString(R.string.event_info_title));
1049         }
1050     }
1051 
1052     /**
1053      * Initializes the event cursor, which is expected to point to the first
1054      * (and only) result from a query.
1055      * @return false if the cursor is empty, true otherwise
1056      */
initEventCursor()1057     private boolean initEventCursor() {
1058         if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
1059             return false;
1060         }
1061         mEventCursor.moveToFirst();
1062         mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
1063         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
1064         mIsRepeating = !TextUtils.isEmpty(rRule);
1065         // mHasAlarm will be true if it was saved in the event already, or if
1066         // we've explicitly been provided reminders (e.g. during rotation).
1067         mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)? true :
1068             (mReminders != null && mReminders.size() > 0);
1069         mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS);
1070         mCalendarAllowedReminders =  mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS);
1071         return true;
1072     }
1073 
1074     @SuppressWarnings("fallthrough")
initAttendeesCursor(View view)1075     private void initAttendeesCursor(View view) {
1076         mOriginalAttendeeResponse = Attendees.ATTENDEE_STATUS_NONE;
1077         mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
1078         mNumOfAttendees = 0;
1079         if (mAttendeesCursor != null) {
1080             mNumOfAttendees = mAttendeesCursor.getCount();
1081             if (mAttendeesCursor.moveToFirst()) {
1082                 mAcceptedAttendees.clear();
1083                 mDeclinedAttendees.clear();
1084                 mTentativeAttendees.clear();
1085                 mNoResponseAttendees.clear();
1086 
1087                 do {
1088                     int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
1089                     String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
1090                     String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
1091 
1092                     if (mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP) ==
1093                             Attendees.RELATIONSHIP_ORGANIZER) {
1094 
1095                         // Overwrites the one from Event table if available
1096                         if (!TextUtils.isEmpty(name)) {
1097                             mEventOrganizerDisplayName = name;
1098                             if (!mIsOrganizer) {
1099                                 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1100                                 setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName);
1101                             }
1102                         }
1103                     }
1104 
1105                     if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
1106                             mCalendarOwnerAccount.equalsIgnoreCase(email)) {
1107                         mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
1108                         mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
1109                     } else {
1110                         String identity = null;
1111                         String idNamespace = null;
1112 
1113                         if (Utils.isJellybeanOrLater()) {
1114                             identity = mAttendeesCursor.getString(ATTENDEES_INDEX_IDENTITY);
1115                             idNamespace = mAttendeesCursor.getString(ATTENDEES_INDEX_ID_NAMESPACE);
1116                         }
1117 
1118                         // Don't show your own status in the list because:
1119                         //  1) it doesn't make sense for event without other guests.
1120                         //  2) there's a spinner for that for events with guests.
1121                         switch(status) {
1122                             case Attendees.ATTENDEE_STATUS_ACCEPTED:
1123                                 mAcceptedAttendees.add(new Attendee(name, email,
1124                                         Attendees.ATTENDEE_STATUS_ACCEPTED, identity,
1125                                         idNamespace));
1126                                 break;
1127                             case Attendees.ATTENDEE_STATUS_DECLINED:
1128                                 mDeclinedAttendees.add(new Attendee(name, email,
1129                                         Attendees.ATTENDEE_STATUS_DECLINED, identity,
1130                                         idNamespace));
1131                                 break;
1132                             case Attendees.ATTENDEE_STATUS_TENTATIVE:
1133                                 mTentativeAttendees.add(new Attendee(name, email,
1134                                         Attendees.ATTENDEE_STATUS_TENTATIVE, identity,
1135                                         idNamespace));
1136                                 break;
1137                             default:
1138                                 mNoResponseAttendees.add(new Attendee(name, email,
1139                                         Attendees.ATTENDEE_STATUS_NONE, identity,
1140                                         idNamespace));
1141                         }
1142                     }
1143                 } while (mAttendeesCursor.moveToNext());
1144                 mAttendeesCursor.moveToFirst();
1145 
1146                 updateAttendees(view);
1147             }
1148         }
1149     }
1150 
1151     @Override
onSaveInstanceState(Bundle outState)1152     public void onSaveInstanceState(Bundle outState) {
1153         super.onSaveInstanceState(outState);
1154         outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
1155         outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
1156         outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
1157         outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
1158         outState.putInt(BUNDLE_KEY_WINDOW_STYLE, mWindowStyle);
1159         outState.putBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE, mDeleteDialogVisible);
1160         outState.putInt(BUNDLE_KEY_CALENDAR_COLOR, mCalendarColor);
1161         outState.putBoolean(BUNDLE_KEY_CALENDAR_COLOR_INIT, mCalendarColorInitialized);
1162         outState.putInt(BUNDLE_KEY_ORIGINAL_COLOR, mOriginalColor);
1163         outState.putBoolean(BUNDLE_KEY_ORIGINAL_COLOR_INIT, mOriginalColorInitialized);
1164         outState.putInt(BUNDLE_KEY_CURRENT_COLOR, mCurrentColor);
1165         outState.putBoolean(BUNDLE_KEY_CURRENT_COLOR_INIT, mCurrentColorInitialized);
1166         outState.putInt(BUNDLE_KEY_CURRENT_COLOR_KEY, mCurrentColorKey);
1167 
1168         // We'll need the temporary response for configuration changes.
1169         outState.putInt(BUNDLE_KEY_TENTATIVE_USER_RESPONSE, mTentativeUserSetResponse);
1170         if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE &&
1171                 mEditResponseHelper != null) {
1172             outState.putInt(BUNDLE_KEY_RESPONSE_WHICH_EVENTS,
1173                     mEditResponseHelper.getWhichEvents());
1174         }
1175 
1176         // Save the current response.
1177         int response;
1178         if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
1179             response = mAttendeeResponseFromIntent;
1180         } else {
1181             response = mOriginalAttendeeResponse;
1182         }
1183         outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, response);
1184         if (mUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
1185             response = mUserSetResponse;
1186             outState.putInt(BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE, response);
1187             outState.putInt(BUNDLE_KEY_RESPONSE_WHICH_EVENTS, mWhichEvents);
1188         }
1189 
1190         // Save the reminders.
1191         mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
1192                 mReminderMinuteValues, mReminderMethodValues);
1193         int numReminders = mReminders.size();
1194         ArrayList<Integer> reminderMinutes =
1195                 new ArrayList<Integer>(numReminders);
1196         ArrayList<Integer> reminderMethods =
1197                 new ArrayList<Integer>(numReminders);
1198         for (ReminderEntry reminder : mReminders) {
1199             reminderMinutes.add(reminder.getMinutes());
1200             reminderMethods.add(reminder.getMethod());
1201         }
1202         outState.putIntegerArrayList(
1203                 BUNDLE_KEY_REMINDER_MINUTES, reminderMinutes);
1204         outState.putIntegerArrayList(
1205                 BUNDLE_KEY_REMINDER_METHODS, reminderMethods);
1206     }
1207 
1208     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)1209     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
1210         super.onCreateOptionsMenu(menu, inflater);
1211         // Show color/edit/delete buttons only in non-dialog configuration
1212         if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
1213             inflater.inflate(R.menu.event_info_title_bar, menu);
1214             mMenu = menu;
1215             updateMenu();
1216         }
1217     }
1218 
1219     @Override
onOptionsItemSelected(MenuItem item)1220     public boolean onOptionsItemSelected(MenuItem item) {
1221 
1222         // If we're a dialog we don't want to handle menu buttons
1223         if (mIsDialog) {
1224             return false;
1225         }
1226         // Handles option menu selections:
1227         // Home button - close event info activity and start the main calendar
1228         // one
1229         // Edit button - start the event edit activity and close the info
1230         // activity
1231         // Delete button - start a delete query that calls a runnable that close
1232         // the info activity
1233 
1234         final int itemId = item.getItemId();
1235         if (itemId == android.R.id.home) {
1236             Utils.returnToCalendarHome(mContext);
1237             mActivity.finish();
1238             return true;
1239         } else if (itemId == R.id.info_action_edit) {
1240             doEdit();
1241             mActivity.finish();
1242         } else if (itemId == R.id.info_action_delete) {
1243             mDeleteHelper =
1244                     new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */);
1245             mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
1246             mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
1247             mDeleteDialogVisible = true;
1248             mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
1249         } else if (itemId == R.id.info_action_change_color) {
1250             showEventColorPickerDialog();
1251         }
1252         return super.onOptionsItemSelected(item);
1253     }
1254 
showEventColorPickerDialog()1255     private void showEventColorPickerDialog() {
1256         if (mColorPickerDialog == null) {
1257             mColorPickerDialog = EventColorPickerDialog.newInstance(mColors, mCurrentColor,
1258                     mCalendarColor, mIsTabletConfig);
1259             mColorPickerDialog.setOnColorSelectedListener(this);
1260         }
1261         final FragmentManager fragmentManager = getFragmentManager();
1262         fragmentManager.executePendingTransactions();
1263         if (!mColorPickerDialog.isAdded()) {
1264             mColorPickerDialog.show(fragmentManager, COLOR_PICKER_DIALOG_TAG);
1265         }
1266     }
1267 
saveEventColor()1268     private boolean saveEventColor() {
1269         if (mCurrentColor == mOriginalColor) {
1270             return false;
1271         }
1272 
1273         ContentValues values = new ContentValues();
1274         if (mCurrentColor != mCalendarColor) {
1275             values.put(Events.EVENT_COLOR_KEY, mCurrentColorKey);
1276         } else {
1277             values.put(Events.EVENT_COLOR_KEY, NO_EVENT_COLOR);
1278         }
1279         Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1280         mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
1281                 null, null, Utils.UNDO_DELAY);
1282         return true;
1283     }
1284 
1285     @Override
onStop()1286     public void onStop() {
1287         Activity act = getActivity();
1288         if (!mEventDeletionStarted && act != null && !act.isChangingConfigurations()) {
1289 
1290             boolean responseSaved = saveResponse();
1291             boolean eventColorSaved = saveEventColor();
1292             if (saveReminders() || responseSaved || eventColorSaved) {
1293                 Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
1294             }
1295         }
1296         super.onStop();
1297     }
1298 
1299     @Override
onDestroy()1300     public void onDestroy() {
1301         if (mEventCursor != null) {
1302             mEventCursor.close();
1303         }
1304         if (mCalendarsCursor != null) {
1305             mCalendarsCursor.close();
1306         }
1307         if (mAttendeesCursor != null) {
1308             mAttendeesCursor.close();
1309         }
1310         super.onDestroy();
1311     }
1312 
1313     /**
1314      * Asynchronously saves the response to an invitation if the user changed
1315      * the response. Returns true if the database will be updated.
1316      *
1317      * @return true if the database will be changed
1318      */
saveResponse()1319     private boolean saveResponse() {
1320         if (mAttendeesCursor == null || mEventCursor == null) {
1321             return false;
1322         }
1323 
1324         int status = getResponseFromButtonId(
1325                 mResponseRadioGroup.getCheckedRadioButtonId());
1326         if (status == Attendees.ATTENDEE_STATUS_NONE) {
1327             return false;
1328         }
1329 
1330         // If the status has not changed, then don't update the database
1331         if (status == mOriginalAttendeeResponse) {
1332             return false;
1333         }
1334 
1335         // If we never got an owner attendee id we can't set the status
1336         if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
1337             return false;
1338         }
1339 
1340         if (!mIsRepeating) {
1341             // This is a non-repeating event
1342             updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
1343             mOriginalAttendeeResponse = status;
1344             return true;
1345         }
1346 
1347         if (DEBUG) {
1348             Log.d(TAG, "Repeating event: mWhichEvents=" + mWhichEvents);
1349         }
1350         // This is a repeating event
1351         switch (mWhichEvents) {
1352             case -1:
1353                 return false;
1354             case UPDATE_SINGLE:
1355                 createExceptionResponse(mEventId, status);
1356                 mOriginalAttendeeResponse = status;
1357                 return true;
1358             case UPDATE_ALL:
1359                 updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
1360                 mOriginalAttendeeResponse = status;
1361                 return true;
1362             default:
1363                 Log.e(TAG, "Unexpected choice for updating invitation response");
1364                 break;
1365         }
1366         return false;
1367     }
1368 
updateResponse(long eventId, long attendeeId, int status)1369     private void updateResponse(long eventId, long attendeeId, int status) {
1370         // Update the attendee status in the attendees table.  the provider
1371         // takes care of updating the self attendance status.
1372         ContentValues values = new ContentValues();
1373 
1374         if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
1375             values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
1376         }
1377         values.put(Attendees.ATTENDEE_STATUS, status);
1378         values.put(Attendees.EVENT_ID, eventId);
1379 
1380         Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
1381 
1382         mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
1383                 null, null, Utils.UNDO_DELAY);
1384     }
1385 
1386     /**
1387      * Creates an exception to a recurring event.  The only change we're making is to the
1388      * "self attendee status" value.  The provider will take care of updating the corresponding
1389      * Attendees.attendeeStatus entry.
1390      *
1391      * @param eventId The recurring event.
1392      * @param status The new value for selfAttendeeStatus.
1393      */
createExceptionResponse(long eventId, int status)1394     private void createExceptionResponse(long eventId, int status) {
1395         ContentValues values = new ContentValues();
1396         values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
1397         values.put(Events.SELF_ATTENDEE_STATUS, status);
1398         values.put(Events.STATUS, Events.STATUS_CONFIRMED);
1399 
1400         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1401         Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
1402                 String.valueOf(eventId));
1403         ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build());
1404 
1405         mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops,
1406                 Utils.UNDO_DELAY);
1407    }
1408 
getResponseFromButtonId(int buttonId)1409     public static int getResponseFromButtonId(int buttonId) {
1410         int response;
1411         if (buttonId == R.id.response_yes) {
1412             response = Attendees.ATTENDEE_STATUS_ACCEPTED;
1413         } else if (buttonId == R.id.response_maybe) {
1414             response = Attendees.ATTENDEE_STATUS_TENTATIVE;
1415         } else if (buttonId == R.id.response_no) {
1416             response = Attendees.ATTENDEE_STATUS_DECLINED;
1417         } else {
1418             response = Attendees.ATTENDEE_STATUS_NONE;
1419         }
1420         return response;
1421     }
1422 
findButtonIdForResponse(int response)1423     public static int findButtonIdForResponse(int response) {
1424         int buttonId;
1425         switch (response) {
1426             case Attendees.ATTENDEE_STATUS_ACCEPTED:
1427                 buttonId = R.id.response_yes;
1428                 break;
1429             case Attendees.ATTENDEE_STATUS_TENTATIVE:
1430                 buttonId = R.id.response_maybe;
1431                 break;
1432             case Attendees.ATTENDEE_STATUS_DECLINED:
1433                 buttonId = R.id.response_no;
1434                 break;
1435                 default:
1436                     buttonId = -1;
1437         }
1438         return buttonId;
1439     }
1440 
doEdit()1441     private void doEdit() {
1442         Context c = getActivity();
1443         // This ensures that we aren't in the process of closing and have been
1444         // unattached already
1445         if (c != null) {
1446             Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1447             Intent intent = new Intent(Intent.ACTION_EDIT, uri);
1448             intent.setClass(mActivity, EditEventActivity.class);
1449             intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
1450             intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis);
1451             intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay);
1452             intent.putExtra(EditEventActivity.EXTRA_EVENT_COLOR, mCurrentColor);
1453             intent.putExtra(EditEventActivity.EXTRA_EVENT_REMINDERS, EventViewUtils
1454                     .reminderItemsToReminders(mReminderViews, mReminderMinuteValues,
1455                     mReminderMethodValues));
1456             intent.putExtra(EVENT_EDIT_ON_LAUNCH, true);
1457             startActivity(intent);
1458         }
1459     }
1460 
displayEventNotFound()1461     private void displayEventNotFound() {
1462         mErrorMsgView.setVisibility(View.VISIBLE);
1463         mScrollView.setVisibility(View.GONE);
1464         mLoadingMsgView.setVisibility(View.GONE);
1465     }
1466 
updateEvent(View view)1467     private void updateEvent(View view) {
1468         if (mEventCursor == null || view == null) {
1469             return;
1470         }
1471 
1472         Context context = view.getContext();
1473         if (context == null) {
1474             return;
1475         }
1476 
1477         String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
1478         if (eventName == null || eventName.length() == 0) {
1479             eventName = getActivity().getString(R.string.no_title_label);
1480         }
1481 
1482         // 3rd parties might not have specified the start/end time when firing the
1483         // Events.CONTENT_URI intent.  Update these with values read from the db.
1484         if (mStartMillis == 0 && mEndMillis == 0) {
1485             mStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1486             mEndMillis = mEventCursor.getLong(EVENT_INDEX_DTEND);
1487             if (mEndMillis == 0) {
1488                 String duration = mEventCursor.getString(EVENT_INDEX_DURATION);
1489                 if (!TextUtils.isEmpty(duration)) {
1490                     try {
1491                         Duration d = new Duration();
1492                         d.parse(duration);
1493                         long endMillis = mStartMillis + d.getMillis();
1494                         if (endMillis >= mStartMillis) {
1495                             mEndMillis = endMillis;
1496                         } else {
1497                             Log.d(TAG, "Invalid duration string: " + duration);
1498                         }
1499                     } catch (DateException e) {
1500                         Log.d(TAG, "Error parsing duration string " + duration, e);
1501                     }
1502                 }
1503                 if (mEndMillis == 0) {
1504                     mEndMillis = mStartMillis;
1505                 }
1506             }
1507         }
1508 
1509         mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1510         String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
1511         String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
1512         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
1513         String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
1514 
1515         mHeadlines.setBackgroundColor(mCurrentColor);
1516 
1517         // What
1518         if (eventName != null) {
1519             setTextCommon(view, R.id.title, eventName);
1520         }
1521 
1522         // When
1523         // Set the date and repeats (if any)
1524         String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater);
1525 
1526         Resources resources = context.getResources();
1527         String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis,
1528                 System.currentTimeMillis(), localTimezone, mAllDay, context);
1529 
1530         String displayedTimezone = null;
1531         if (!mAllDay) {
1532             displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone,
1533                     eventTimezone);
1534         }
1535         // Display the datetime.  Make the timezone (if any) transparent.
1536         if (displayedTimezone == null) {
1537             setTextCommon(view, R.id.when_datetime, displayedDatetime);
1538         } else {
1539             int timezoneIndex = displayedDatetime.length();
1540             displayedDatetime += "  " + displayedTimezone;
1541             SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime);
1542             ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan(
1543                     resources.getColor(R.color.event_info_headline_transparent_color));
1544             sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(),
1545                     Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1546             setTextCommon(view, R.id.when_datetime, sb);
1547         }
1548 
1549         // Display the repeat string (if any)
1550         String repeatString = null;
1551         if (!TextUtils.isEmpty(rRule)) {
1552             EventRecurrence eventRecurrence = new EventRecurrence();
1553             eventRecurrence.parse(rRule);
1554             Time date = new Time(localTimezone);
1555             date.set(mStartMillis);
1556             if (mAllDay) {
1557                 date.timezone = Time.TIMEZONE_UTC;
1558             }
1559             eventRecurrence.setStartDate(date);
1560             repeatString = EventRecurrenceFormatter.getRepeatString(mContext, resources,
1561                     eventRecurrence, true);
1562         }
1563         if (repeatString == null) {
1564             view.findViewById(R.id.when_repeat).setVisibility(View.GONE);
1565         } else {
1566             setTextCommon(view, R.id.when_repeat, repeatString);
1567         }
1568 
1569         // Organizer view is setup in the updateCalendar method
1570 
1571 
1572         // Where
1573         if (location == null || location.trim().length() == 0) {
1574             setVisibilityCommon(view, R.id.where, View.GONE);
1575         } else {
1576             final TextView textView = mWhere;
1577             if (textView != null) {
1578                 textView.setAutoLinkMask(0);
1579                 textView.setText(location.trim());
1580                 try {
1581                     textView.setText(Utils.extendedLinkify(textView.getText().toString(), true));
1582 
1583                     // Linkify.addLinks() sets the TextView movement method if it finds any links.
1584                     // We must do the same here, in case linkify by itself did not find any.
1585                     // (This is cloned from Linkify.addLinkMovementMethod().)
1586                     MovementMethod mm = textView.getMovementMethod();
1587                     if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
1588                         if (textView.getLinksClickable()) {
1589                             textView.setMovementMethod(LinkMovementMethod.getInstance());
1590                         }
1591                     }
1592                 } catch (Exception ex) {
1593                     // unexpected
1594                     Log.e(TAG, "Linkification failed", ex);
1595                 }
1596 
1597                 textView.setOnTouchListener(new OnTouchListener() {
1598                     @Override
1599                     public boolean onTouch(View v, MotionEvent event) {
1600                         try {
1601                             return v.onTouchEvent(event);
1602                         } catch (ActivityNotFoundException e) {
1603                             // ignore
1604                             return true;
1605                         }
1606                     }
1607                 });
1608             }
1609         }
1610 
1611         // Description
1612         if (description != null && description.length() != 0) {
1613             mDesc.setText(description);
1614         }
1615 
1616         // Launch Custom App
1617         if (Utils.isJellybeanOrLater()) {
1618             updateCustomAppButton();
1619         }
1620     }
1621 
updateCustomAppButton()1622     private void updateCustomAppButton() {
1623         buttonSetup: {
1624             final Button launchButton = (Button) mView.findViewById(R.id.launch_custom_app_button);
1625             if (launchButton == null)
1626                 break buttonSetup;
1627 
1628             final String customAppPackage = mEventCursor.getString(EVENT_INDEX_CUSTOM_APP_PACKAGE);
1629             final String customAppUri = mEventCursor.getString(EVENT_INDEX_CUSTOM_APP_URI);
1630 
1631             if (TextUtils.isEmpty(customAppPackage) || TextUtils.isEmpty(customAppUri))
1632                 break buttonSetup;
1633 
1634             PackageManager pm = mContext.getPackageManager();
1635             if (pm == null)
1636                 break buttonSetup;
1637 
1638             ApplicationInfo info;
1639             try {
1640                 info = pm.getApplicationInfo(customAppPackage, 0);
1641                 if (info == null)
1642                     break buttonSetup;
1643             } catch (NameNotFoundException e) {
1644                 break buttonSetup;
1645             }
1646 
1647             Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1648             final Intent intent = new Intent(CalendarContract.ACTION_HANDLE_CUSTOM_EVENT, uri);
1649             intent.setPackage(customAppPackage);
1650             intent.putExtra(CalendarContract.EXTRA_CUSTOM_APP_URI, customAppUri);
1651             intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
1652 
1653             // See if we have a taker for our intent
1654             if (pm.resolveActivity(intent, 0) == null)
1655                 break buttonSetup;
1656 
1657             Drawable icon = pm.getApplicationIcon(info);
1658             if (icon != null) {
1659 
1660                 Drawable[] d = launchButton.getCompoundDrawables();
1661                 icon.setBounds(0, 0, mCustomAppIconSize, mCustomAppIconSize);
1662                 launchButton.setCompoundDrawables(icon, d[1], d[2], d[3]);
1663             }
1664 
1665             CharSequence label = pm.getApplicationLabel(info);
1666             if (label != null && label.length() != 0) {
1667                 launchButton.setText(label);
1668             } else if (icon == null) {
1669                 // No icon && no label. Hide button?
1670                 break buttonSetup;
1671             }
1672 
1673             // Launch custom app
1674             launchButton.setOnClickListener(new View.OnClickListener() {
1675                 @Override
1676                 public void onClick(View v) {
1677                     try {
1678                         startActivityForResult(intent, 0);
1679                     } catch (ActivityNotFoundException e) {
1680                         // Shouldn't happen as we checked it already
1681                         setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE);
1682                     }
1683                 }
1684             });
1685 
1686             setVisibilityCommon(mView, R.id.launch_custom_app_container, View.VISIBLE);
1687             return;
1688 
1689         }
1690 
1691         setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE);
1692         return;
1693     }
1694 
sendAccessibilityEvent()1695     private void sendAccessibilityEvent() {
1696         AccessibilityManager am =
1697             (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
1698         if (!am.isEnabled()) {
1699             return;
1700         }
1701 
1702         AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1703         event.setClassName(EventInfoFragment.class.getName());
1704         event.setPackageName(getActivity().getPackageName());
1705         List<CharSequence> text = event.getText();
1706 
1707         addFieldToAccessibilityEvent(text, mTitle, null);
1708         addFieldToAccessibilityEvent(text, mWhenDateTime, null);
1709         addFieldToAccessibilityEvent(text, mWhere, null);
1710         addFieldToAccessibilityEvent(text, null, mDesc);
1711 
1712         if (mResponseRadioGroup.getVisibility() == View.VISIBLE) {
1713             int id = mResponseRadioGroup.getCheckedRadioButtonId();
1714             if (id != View.NO_ID) {
1715                 text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
1716                 text.add((((RadioButton) (mResponseRadioGroup.findViewById(id)))
1717                         .getText() + PERIOD_SPACE));
1718             }
1719         }
1720 
1721         am.sendAccessibilityEvent(event);
1722     }
1723 
addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv, ExpandableTextView etv)1724     private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv,
1725             ExpandableTextView etv) {
1726         CharSequence cs;
1727         if (tv != null) {
1728             cs = tv.getText();
1729         } else if (etv != null) {
1730             cs = etv.getText();
1731         } else {
1732             return;
1733         }
1734 
1735         if (!TextUtils.isEmpty(cs)) {
1736             cs = cs.toString().trim();
1737             if (cs.length() > 0) {
1738                 text.add(cs);
1739                 text.add(PERIOD_SPACE);
1740             }
1741         }
1742     }
1743 
updateCalendar(View view)1744     private void updateCalendar(View view) {
1745 
1746         mCalendarOwnerAccount = "";
1747         if (mCalendarsCursor != null && mEventCursor != null) {
1748             mCalendarsCursor.moveToFirst();
1749             String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1750             mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1751             mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1752             mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME);
1753 
1754             // start visible calendars query
1755             mHandler.startQuery(TOKEN_QUERY_VISIBLE_CALENDARS, null, Calendars.CONTENT_URI,
1756                     CALENDARS_PROJECTION, CALENDARS_VISIBLE_WHERE, new String[] {"1"}, null);
1757 
1758             mEventOrganizerEmail = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1759             mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(mEventOrganizerEmail);
1760 
1761             if (!TextUtils.isEmpty(mEventOrganizerEmail) &&
1762                     !mEventOrganizerEmail.endsWith(Utils.MACHINE_GENERATED_ADDRESS)) {
1763                 mEventOrganizerDisplayName = mEventOrganizerEmail;
1764             }
1765 
1766             if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) {
1767                 setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName);
1768                 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1769             } else {
1770                 setVisibilityCommon(view, R.id.organizer_container, View.GONE);
1771             }
1772             mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1773             mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL)
1774                     >= Calendars.CAL_ACCESS_CONTRIBUTOR;
1775             // TODO add "|| guestCanModify" after b/1299071 is fixed
1776             mCanModifyEvent = mCanModifyCalendar && mIsOrganizer;
1777             mIsBusyFreeCalendar =
1778                     mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY;
1779 
1780             if (!mIsBusyFreeCalendar) {
1781 
1782                 View b = mView.findViewById(R.id.edit);
1783                 b.setEnabled(true);
1784                 b.setOnClickListener(new OnClickListener() {
1785                     @Override
1786                     public void onClick(View v) {
1787                         doEdit();
1788                         // For dialogs, just close the fragment
1789                         // For full screen, close activity on phone, leave it for tablet
1790                         if (mIsDialog) {
1791                             EventInfoFragment.this.dismiss();
1792                         }
1793                         else if (!mIsTabletConfig){
1794                             getActivity().finish();
1795                         }
1796                     }
1797                 });
1798             }
1799             View button;
1800             if (mCanModifyCalendar) {
1801                 button = mView.findViewById(R.id.delete);
1802                 if (button != null) {
1803                     button.setEnabled(true);
1804                     button.setVisibility(View.VISIBLE);
1805                 }
1806             }
1807             if (mCanModifyEvent) {
1808                 button = mView.findViewById(R.id.edit);
1809                 if (button != null) {
1810                     button.setEnabled(true);
1811                     button.setVisibility(View.VISIBLE);
1812                 }
1813             }
1814             if ((!mIsDialog && !mIsTabletConfig ||
1815                     mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) {
1816                 mActivity.invalidateOptionsMenu();
1817             }
1818         } else {
1819             setVisibilityCommon(view, R.id.calendar, View.GONE);
1820             sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
1821         }
1822     }
1823 
1824     /**
1825      *
1826      */
updateMenu()1827     private void updateMenu() {
1828         if (mMenu == null) {
1829             return;
1830         }
1831         MenuItem delete = mMenu.findItem(R.id.info_action_delete);
1832         MenuItem edit = mMenu.findItem(R.id.info_action_edit);
1833         MenuItem changeColor = mMenu.findItem(R.id.info_action_change_color);
1834         if (delete != null) {
1835             delete.setVisible(mCanModifyCalendar);
1836             delete.setEnabled(mCanModifyCalendar);
1837         }
1838         if (edit != null) {
1839             edit.setVisible(mCanModifyEvent);
1840             edit.setEnabled(mCanModifyEvent);
1841         }
1842         if (changeColor != null && mColors != null && mColors.length > 0) {
1843             changeColor.setVisible(mCanModifyCalendar);
1844             changeColor.setEnabled(mCanModifyCalendar);
1845         }
1846     }
1847 
updateAttendees(View view)1848     private void updateAttendees(View view) {
1849         if (mAcceptedAttendees.size() + mDeclinedAttendees.size() +
1850                 mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) {
1851             mLongAttendees.clearAttendees();
1852             (mLongAttendees).addAttendees(mAcceptedAttendees);
1853             (mLongAttendees).addAttendees(mDeclinedAttendees);
1854             (mLongAttendees).addAttendees(mTentativeAttendees);
1855             (mLongAttendees).addAttendees(mNoResponseAttendees);
1856             mLongAttendees.setEnabled(false);
1857             mLongAttendees.setVisibility(View.VISIBLE);
1858         } else {
1859             mLongAttendees.setVisibility(View.GONE);
1860         }
1861 
1862         if (hasEmailableAttendees()) {
1863             setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE);
1864             if (emailAttendeesButton != null) {
1865                 emailAttendeesButton.setText(R.string.email_guests_label);
1866             }
1867         } else if (hasEmailableOrganizer()) {
1868             setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE);
1869             if (emailAttendeesButton != null) {
1870                 emailAttendeesButton.setText(R.string.email_organizer_label);
1871             }
1872         } else {
1873             setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE);
1874         }
1875     }
1876 
1877     /**
1878      * Returns true if there is at least 1 attendee that is not the viewer.
1879      */
hasEmailableAttendees()1880     private boolean hasEmailableAttendees() {
1881         for (Attendee attendee : mAcceptedAttendees) {
1882             if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1883                 return true;
1884             }
1885         }
1886         for (Attendee attendee : mTentativeAttendees) {
1887             if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1888                 return true;
1889             }
1890         }
1891         for (Attendee attendee : mNoResponseAttendees) {
1892             if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1893                 return true;
1894             }
1895         }
1896         for (Attendee attendee : mDeclinedAttendees) {
1897             if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1898                 return true;
1899             }
1900         }
1901         return false;
1902     }
1903 
hasEmailableOrganizer()1904     private boolean hasEmailableOrganizer() {
1905         return mEventOrganizerEmail != null &&
1906                 Utils.isEmailableFrom(mEventOrganizerEmail, mSyncAccountName);
1907     }
1908 
initReminders(View view, Cursor cursor)1909     public void initReminders(View view, Cursor cursor) {
1910 
1911         // Add reminders
1912         mOriginalReminders.clear();
1913         mUnsupportedReminders.clear();
1914         while (cursor.moveToNext()) {
1915             int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
1916             int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD);
1917 
1918             if (method != Reminders.METHOD_DEFAULT && !mReminderMethodValues.contains(method)) {
1919                 // Stash unsupported reminder types separately so we don't alter
1920                 // them in the UI
1921                 mUnsupportedReminders.add(ReminderEntry.valueOf(minutes, method));
1922             } else {
1923                 mOriginalReminders.add(ReminderEntry.valueOf(minutes, method));
1924             }
1925         }
1926         // Sort appropriately for display (by time, then type)
1927         Collections.sort(mOriginalReminders);
1928 
1929         if (mUserModifiedReminders) {
1930             // If the user has changed the list of reminders don't change what's
1931             // shown.
1932             return;
1933         }
1934 
1935         LinearLayout parent = (LinearLayout) mScrollView
1936                 .findViewById(R.id.reminder_items_container);
1937         if (parent != null) {
1938             parent.removeAllViews();
1939         }
1940         if (mReminderViews != null) {
1941             mReminderViews.clear();
1942         }
1943 
1944         if (mHasAlarm) {
1945             ArrayList<ReminderEntry> reminders;
1946             // If applicable, use reminders saved in the bundle.
1947             if (mReminders != null) {
1948                 reminders = mReminders;
1949             } else {
1950                 reminders = mOriginalReminders;
1951             }
1952             // Insert any minute values that aren't represented in the minutes list.
1953             for (ReminderEntry re : reminders) {
1954                 EventViewUtils.addMinutesToList(
1955                         mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
1956             }
1957             // Create a UI element for each reminder.  We display all of the reminders we get
1958             // from the provider, even if the count exceeds the calendar maximum.  (Also, for
1959             // a new event, we won't have a maxReminders value available.)
1960             for (ReminderEntry re : reminders) {
1961                 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1962                         mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1963                         mReminderMethodLabels, re, Integer.MAX_VALUE, mReminderChangeListener);
1964             }
1965             EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
1966             // TODO show unsupported reminder types in some fashion.
1967         }
1968     }
1969 
updateResponse(View view)1970     void updateResponse(View view) {
1971         // we only let the user accept/reject/etc. a meeting if:
1972         // a) you can edit the event's containing calendar AND
1973         // b) you're not the organizer and only attendee AND
1974         // c) organizerCanRespond is enabled for the calendar
1975         // (if the attendee data has been hidden, the visible number of attendees
1976         // will be 1 -- the calendar owner's).
1977         // (there are more cases involved to be 100% accurate, such as
1978         // paying attention to whether or not an attendee status was
1979         // included in the feed, but we're currently omitting those corner cases
1980         // for simplicity).
1981 
1982         // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1983         if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1984                 (mIsOrganizer && !mOwnerCanRespond)) {
1985             setVisibilityCommon(view, R.id.response_container, View.GONE);
1986             return;
1987         }
1988 
1989         setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1990 
1991 
1992         int response;
1993         if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
1994             response = mTentativeUserSetResponse;
1995         } else if (mUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
1996             response = mUserSetResponse;
1997         } else if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
1998             response = mAttendeeResponseFromIntent;
1999         } else {
2000             response = mOriginalAttendeeResponse;
2001         }
2002 
2003         int buttonToCheck = findButtonIdForResponse(response);
2004         mResponseRadioGroup.check(buttonToCheck); // -1 clear all radio buttons
2005         mResponseRadioGroup.setOnCheckedChangeListener(this);
2006     }
2007 
setTextCommon(View view, int id, CharSequence text)2008     private void setTextCommon(View view, int id, CharSequence text) {
2009         TextView textView = (TextView) view.findViewById(id);
2010         if (textView == null)
2011             return;
2012         textView.setText(text);
2013     }
2014 
setVisibilityCommon(View view, int id, int visibility)2015     private void setVisibilityCommon(View view, int id, int visibility) {
2016         View v = view.findViewById(id);
2017         if (v != null) {
2018             v.setVisibility(visibility);
2019         }
2020         return;
2021     }
2022 
2023     /**
2024      * Taken from com.google.android.gm.HtmlConversationActivity
2025      *
2026      * Send the intent that shows the Contact info corresponding to the email address.
2027      */
showContactInfo(Attendee attendee, Rect rect)2028     public void showContactInfo(Attendee attendee, Rect rect) {
2029         // First perform lookup query to find existing contact
2030         final ContentResolver resolver = getActivity().getContentResolver();
2031         final String address = attendee.mEmail;
2032         final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
2033                 Uri.encode(address));
2034         final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
2035 
2036         if (lookupUri != null) {
2037             // Found matching contact, trigger QuickContact
2038             QuickContact.showQuickContact(getActivity(), rect, lookupUri,
2039                     QuickContact.MODE_MEDIUM, null);
2040         } else {
2041             // No matching contact, ask user to create one
2042             final Uri mailUri = Uri.fromParts("mailto", address, null);
2043             final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
2044 
2045             // Pass along full E-mail string for possible create dialog
2046             Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
2047             intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
2048 
2049             // Only provide personal name hint if we have one
2050             final String senderPersonal = attendee.mName;
2051             if (!TextUtils.isEmpty(senderPersonal)) {
2052                 intent.putExtra(Intents.Insert.NAME, senderPersonal);
2053             }
2054 
2055             startActivity(intent);
2056         }
2057     }
2058 
2059     @Override
onPause()2060     public void onPause() {
2061         mIsPaused = true;
2062         mHandler.removeCallbacks(onDeleteRunnable);
2063         super.onPause();
2064         // Remove event deletion alert box since it is being rebuild in the OnResume
2065         // This is done to get the same behavior on OnResume since the AlertDialog is gone on
2066         // rotation but not if you press the HOME key
2067         if (mDeleteDialogVisible && mDeleteHelper != null) {
2068             mDeleteHelper.dismissAlertDialog();
2069             mDeleteHelper = null;
2070         }
2071         if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE
2072                 && mEditResponseHelper != null) {
2073             mEditResponseHelper.dismissAlertDialog();
2074         }
2075     }
2076 
2077     @Override
onResume()2078     public void onResume() {
2079         super.onResume();
2080         if (mIsDialog) {
2081             setDialogSize(getActivity().getResources());
2082             applyDialogParams();
2083         }
2084         mIsPaused = false;
2085         if (mDismissOnResume) {
2086             mHandler.post(onDeleteRunnable);
2087         }
2088         // Display the "delete confirmation" or "edit response helper" dialog if needed
2089         if (mDeleteDialogVisible) {
2090             mDeleteHelper = new DeleteEventHelper(
2091                     mContext, mActivity,
2092                     !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
2093             mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
2094             mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
2095         } else if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
2096             int buttonId = findButtonIdForResponse(mTentativeUserSetResponse);
2097             mResponseRadioGroup.check(buttonId);
2098             mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
2099         }
2100     }
2101 
2102     @Override
eventsChanged()2103     public void eventsChanged() {
2104     }
2105 
2106     @Override
getSupportedEventTypes()2107     public long getSupportedEventTypes() {
2108         return EventType.EVENTS_CHANGED;
2109     }
2110 
2111     @Override
handleEvent(EventInfo event)2112     public void handleEvent(EventInfo event) {
2113         reloadEvents();
2114     }
2115 
reloadEvents()2116     public void reloadEvents() {
2117         if (mHandler != null) {
2118             mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
2119                     null, null, null);
2120         }
2121     }
2122 
2123     @Override
onClick(View view)2124     public void onClick(View view) {
2125 
2126         // This must be a click on one of the "remove reminder" buttons
2127         LinearLayout reminderItem = (LinearLayout) view.getParent();
2128         LinearLayout parent = (LinearLayout) reminderItem.getParent();
2129         parent.removeView(reminderItem);
2130         mReminderViews.remove(reminderItem);
2131         mUserModifiedReminders = true;
2132         EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
2133     }
2134 
2135 
2136     /**
2137      * Add a new reminder when the user hits the "add reminder" button.  We use the default
2138      * reminder time and method.
2139      */
addReminder()2140     private void addReminder() {
2141         // TODO: when adding a new reminder, make it different from the
2142         // last one in the list (if any).
2143         if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
2144             EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
2145                     mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
2146                     mReminderMethodLabels,
2147                     ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mMaxReminders,
2148                     mReminderChangeListener);
2149         } else {
2150             EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
2151                     mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
2152                     mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes),
2153                     mMaxReminders, mReminderChangeListener);
2154         }
2155 
2156         EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
2157     }
2158 
prepareReminders()2159     synchronized private void prepareReminders() {
2160         // Nothing to do if we've already built these lists _and_ we aren't
2161         // removing not allowed methods
2162         if (mReminderMinuteValues != null && mReminderMinuteLabels != null
2163                 && mReminderMethodValues != null && mReminderMethodLabels != null
2164                 && mCalendarAllowedReminders == null) {
2165             return;
2166         }
2167         // Load the labels and corresponding numeric values for the minutes and methods lists
2168         // from the assets.  If we're switching calendars, we need to clear and re-populate the
2169         // lists (which may have elements added and removed based on calendar properties).  This
2170         // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
2171         // new event that aren't in the default set.
2172         Resources r = mActivity.getResources();
2173         mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
2174         mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
2175         mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
2176         mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
2177 
2178         // Remove any reminder methods that aren't allowed for this calendar.  If this is
2179         // a new event, mCalendarAllowedReminders may not be set the first time we're called.
2180         if (mCalendarAllowedReminders != null) {
2181             EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
2182                     mCalendarAllowedReminders);
2183         }
2184         if (mView != null) {
2185             mView.invalidate();
2186         }
2187     }
2188 
2189 
saveReminders()2190     private boolean saveReminders() {
2191         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
2192 
2193         // Read reminders from UI
2194         mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
2195                 mReminderMinuteValues, mReminderMethodValues);
2196         mOriginalReminders.addAll(mUnsupportedReminders);
2197         Collections.sort(mOriginalReminders);
2198         mReminders.addAll(mUnsupportedReminders);
2199         Collections.sort(mReminders);
2200 
2201         // Check if there are any changes in the reminder
2202         boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders,
2203                 mOriginalReminders, false /* no force save */);
2204 
2205         if (!changed) {
2206             return false;
2207         }
2208 
2209         // save new reminders
2210         AsyncQueryService service = new AsyncQueryService(getActivity());
2211         service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0);
2212         mOriginalReminders = mReminders;
2213         // Update the "hasAlarm" field for the event
2214         Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
2215         int len = mReminders.size();
2216         boolean hasAlarm = len > 0;
2217         if (hasAlarm != mHasAlarm) {
2218             ContentValues values = new ContentValues();
2219             values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
2220             service.startUpdate(0, null, uri, values, null, null, 0);
2221         }
2222         return true;
2223     }
2224 
2225     /**
2226      * Email all the attendees of the event, except for the viewer (so as to not email
2227      * himself) and resources like conference rooms.
2228      */
emailAttendees()2229     private void emailAttendees() {
2230         Intent i = new Intent(getActivity(), QuickResponseActivity.class);
2231         i.putExtra(QuickResponseActivity.EXTRA_EVENT_ID, mEventId);
2232         i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2233         startActivity(i);
2234     }
2235 
2236     /**
2237      * Loads an integer array asset into a list.
2238      */
loadIntegerArray(Resources r, int resNum)2239     private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
2240         int[] vals = r.getIntArray(resNum);
2241         int size = vals.length;
2242         ArrayList<Integer> list = new ArrayList<Integer>(size);
2243 
2244         for (int i = 0; i < size; i++) {
2245             list.add(vals[i]);
2246         }
2247 
2248         return list;
2249     }
2250     /**
2251      * Loads a String array asset into a list.
2252      */
loadStringArray(Resources r, int resNum)2253     private static ArrayList<String> loadStringArray(Resources r, int resNum) {
2254         String[] labels = r.getStringArray(resNum);
2255         ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
2256         return list;
2257     }
2258 
2259     @Override
onDeleteStarted()2260     public void onDeleteStarted() {
2261         mEventDeletionStarted = true;
2262     }
2263 
createDeleteOnDismissListener()2264     private Dialog.OnDismissListener createDeleteOnDismissListener() {
2265         return new Dialog.OnDismissListener() {
2266                     @Override
2267                     public void onDismiss(DialogInterface dialog) {
2268                         // Since OnPause will force the dialog to dismiss , do
2269                         // not change the dialog status
2270                         if (!mIsPaused) {
2271                             mDeleteDialogVisible = false;
2272                         }
2273                     }
2274                 };
2275     }
2276 
2277     public long getEventId() {
2278         return mEventId;
2279     }
2280 
2281     public long getStartMillis() {
2282         return mStartMillis;
2283     }
2284     public long getEndMillis() {
2285         return mEndMillis;
2286     }
2287     private void setDialogSize(Resources r) {
2288         mDialogWidth = (int)r.getDimension(R.dimen.event_info_dialog_width);
2289         mDialogHeight = (int)r.getDimension(R.dimen.event_info_dialog_height);
2290     }
2291 
2292     @Override
2293     public void onColorSelected(int color) {
2294         mCurrentColor = color;
2295         mCurrentColorKey = mDisplayColorKeyMap.get(color);
2296         mHeadlines.setBackgroundColor(color);
2297     }
2298 }
2299