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