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