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