diff options
author | Linux Build Service Account <lnxbuild@localhost> | 2015-01-24 07:55:08 -0800 |
---|---|---|
committer | Gerrit - the friendly Code Review server <code-review@localhost> | 2015-01-24 07:55:08 -0800 |
commit | b8508df0bf9a1ab57c4d339a0fc8e60d55239462 (patch) | |
tree | e3d3f10c1b11fa5b37d5592b8b1535b307446328 | |
parent | 476b55527a1d92fe689dd5debd85ac0a00fb3a1b (diff) | |
parent | c0b844cd8a5209895fdc52d5ffa68bf5b2fc1234 (diff) | |
download | android_packages_apps_Calendar-b8508df0bf9a1ab57c4d339a0fc8e60d55239462.tar.gz android_packages_apps_Calendar-b8508df0bf9a1ab57c4d339a0fc8e60d55239462.tar.bz2 android_packages_apps_Calendar-b8508df0bf9a1ab57c4d339a0fc8e60d55239462.zip |
Merge "Calendar : Respond to calendar events share intent"
-rw-r--r-- | AndroidManifest.xml | 10 | ||||
-rw-r--r-- | res/layout/agenda_item.xml | 19 | ||||
-rw-r--r-- | res/menu/share_event_title_bar.xml | 29 | ||||
-rw-r--r-- | res/values/cm_plurals.xml | 11 | ||||
-rw-r--r-- | src/com/android/calendar/CalendarUtils.java | 77 | ||||
-rw-r--r-- | src/com/android/calendar/EventInfoFragment.java | 251 | ||||
-rw-r--r-- | src/com/android/calendar/ShareCalendarActivity.java | 229 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaAdapter.java | 3 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaFragment.java | 33 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaListView.java | 43 | ||||
-rw-r--r-- | src/com/android/calendar/agenda/AgendaWindowAdapter.java | 60 | ||||
-rw-r--r-- | src/com/android/calendar/icalendar/VCalendar.java | 21 |
12 files changed, 668 insertions, 118 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bb4bf3e3..bc36311d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -178,6 +178,16 @@ android:value="com.android.calendar.AllInOneActivity" /> </activity> + <activity android:name=".ShareCalendarActivity" + android:theme="@style/CalendarTheme.WithActionBar"> + <intent-filter> + <action android:name="android.intent.action.GET_CONTENT"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.APP_CALENDAR" /> + <data android:mimeType="text/x-vCalendar"/> + </intent-filter> + </activity> + <provider android:name=".CalendarRecentSuggestionsProvider" android:exported="false" android:authorities="com.android.calendar.CalendarRecentSuggestionsProvider" /> diff --git a/res/layout/agenda_item.xml b/res/layout/agenda_item.xml index 16f81506..8d90581f 100644 --- a/res/layout/agenda_item.xml +++ b/res/layout/agenda_item.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:minHeight="64dip" - android:columnCount="3" + android:columnCount="4" android:rowCount="2"> <View android:layout_height="1px" @@ -28,7 +28,7 @@ android:layout_column="0" android:layout_row="0" android:layout_rowSpan="1" - android:layout_columnSpan="3" + android:layout_columnSpan="4" android:layout_width="match_parent" /> <com.android.calendar.ColorChipView android:id="@+id/agenda_item_color" @@ -86,9 +86,22 @@ android:textColor="@color/agenda_item_where_text_color" style="?android:attr/textAppearanceSmallInverse" /> </LinearLayout> + + <CheckBox + android:layout_width="24dp" + android:layout_height="24dp" + android:id="@+id/shareCheckbox" + android:layout_row="1" + android:layout_column="2" + android:layout_rowSpan="1" + android:layout_gravity="center_vertical" + android:layout_marginRight="10dip" + android:focusable="false" + android:visibility="gone"/> + <ImageView android:id="@+id/selected_marker" - android:layout_column="2" + android:layout_column="3" android:layout_row="1" android:layout_rowSpan="1" android:layout_width="wrap_content" diff --git a/res/menu/share_event_title_bar.xml b/res/menu/share_event_title_bar.xml new file mode 100644 index 00000000..3f8b326c --- /dev/null +++ b/res/menu/share_event_title_bar.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<menu + xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_cancel" + android:alphabeticShortcut="c" + android:title="@string/discard_label" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:showAsAction="withText|always" /> + <item android:id="@+id/action_done" + android:alphabeticShortcut="d" + android:title="@string/save_label" + android:icon="@drawable/ic_menu_done_holo_light" + android:showAsAction="withText|always" /> +</menu> diff --git a/res/values/cm_plurals.xml b/res/values/cm_plurals.xml index d7caea6f..48fae5ab 100644 --- a/res/values/cm_plurals.xml +++ b/res/values/cm_plurals.xml @@ -15,7 +15,16 @@ limitations under the License. --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <plurals name="events_selected"> - <item quantity="other">%1$d selected</item> + <item quantity="zero">No events selected</item> + <item quantity="one">%1$d event selected</item> + <item quantity="other">%1$d events selected</item> + </plurals> + + <plurals name="select_events_to_share"> + <item quantity="one">Select an event to share</item> + <item quantity="other">Select events to share</item> </plurals> + </resources> diff --git a/src/com/android/calendar/CalendarUtils.java b/src/com/android/calendar/CalendarUtils.java index 0238c321..7b9ffc10 100644 --- a/src/com/android/calendar/CalendarUtils.java +++ b/src/com/android/calendar/CalendarUtils.java @@ -103,7 +103,7 @@ public class CalendarUtils { // Check the values in the db int keyColumn = cursor.getColumnIndexOrThrow(CalendarCache.KEY); int valueColumn = cursor.getColumnIndexOrThrow(CalendarCache.VALUE); - while(cursor.moveToNext()) { + while (cursor.moveToNext()) { String key = cursor.getString(keyColumn); String value = cursor.getString(valueColumn); if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) { @@ -353,4 +353,79 @@ public class CalendarUtils { public static SharedPreferences getSharedPreferences(Context context, String prefsName) { return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); } + + /** + * For those interested in changes to the list of shared calendar events + */ + public static interface ShareEventListener { + + /** + * Called whenever a calendar event is added to the list of events about to be shared + * + * @param eventInfo A Triple containing event related information + * Triple (event id, startMillis, endMillis) + */ + public void onEventShared(Triple<Long, Long, Long> eventInfo); + + + /** + * called when an event is removed from the share list + */ + public void onEventRemoval(long eventId); + + /** + * called to signal that the current share list has been discarded + */ + public void onResetShareList(); + } + + /** + * Implementation of a 3-tuple + * Modeled after {@link android.util.Pair} for passing calendar event information + * + * The first element is perceived to be the unique identifier with the others serving as storage + * for ancillary information + */ + public static class Triple<X, Y, Z> { + public X first; // dominating element used in equals() and hashCode() implementations + public Y second; + public Z third; + + public Triple(X a, Y b, Z c) { + first = a; + second = b; + third = c; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + builder.append(first.toString()); + builder.append(" , "); + builder.append(second.toString()); + builder.append(" , "); + builder.append(third.toString()); + builder.append("]"); + return builder.toString(); + } + + /** + * First element in the triple is used for comparison, as the other are used to store + * ancillary information + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Triple)) return false; + return ((Triple)o).first.equals(first); + } + + /** + * Only the first element contributes + */ + @Override + public int hashCode() { + return first.hashCode(); + } + } } diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java index 4374226e..dd4d17a6 100644 --- a/src/com/android/calendar/EventInfoFragment.java +++ b/src/com/android/calendar/EventInfoFragment.java @@ -427,6 +427,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private QueryHandler mHandler; + // used to signal the completion of querying calendar event data + // note: runnable is executed on the ui thread + private Runnable mQueryCompleteRunnable; + private final Runnable mTZUpdater = new Runnable() { @Override public void run() { @@ -463,6 +467,8 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private CalendarController mController; + private boolean mInNonUiMode; + private class QueryHandler extends AsyncQueryService { public QueryHandler(Context context) { super(context); @@ -471,7 +477,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // if the activity is finishing, then close the cursor and return - final Activity activity = getActivity(); + final Activity activity = (mActivity != null) ? mActivity : getActivity(); if (activity == null || activity.isFinishing()) { if (cursor != null) { cursor.close(); @@ -517,9 +523,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange case TOKEN_QUERY_CALENDARS: mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); updateCalendar(mView); - // FRAG_TODO fragments shouldn't set the title anymore - updateTitle(); - + if (!mInNonUiMode) { + // FRAG_TODO fragments shouldn't set the title anymore + updateTitle(); + } args = new String[] { mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME), mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_TYPE) }; @@ -535,7 +542,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); } else { - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); + assessQueryCompletion(TOKEN_QUERY_ATTENDEES); } if (mHasAlarm) { // start reminders query @@ -544,10 +551,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange startQuery(TOKEN_QUERY_REMINDERS, null, uri, REMINDERS_PROJECTION, REMINDERS_WHERE, args, null); } else { - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS); + assessQueryCompletion(TOKEN_QUERY_REMINDERS); } break; case TOKEN_QUERY_COLORS: + if (mInNonUiMode) break; ArrayList<Integer> colors = new ArrayList<Integer>(); if (cursor.moveToFirst()) { do @@ -584,11 +592,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange case TOKEN_QUERY_ATTENDEES: mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); initAttendeesCursor(mView); - updateResponse(mView); + if (!mInNonUiMode) updateResponse(mView); break; case TOKEN_QUERY_REMINDERS: mRemindersCursor = Utils.matrixCursorFromCursor(cursor); - initReminders(mView, mRemindersCursor); + if (!mInNonUiMode) initReminders(mView, mRemindersCursor); break; case TOKEN_QUERY_VISIBLE_CALENDARS: if (cursor.getCount() > 1) { @@ -601,34 +609,38 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } else { // Don't need to display the calendar owner when there is only a single // calendar. Skip the duplicate calendars query. - setVisibilityCommon(mView, R.id.calendar_container, View.GONE); + if (!mInNonUiMode) { + setVisibilityCommon(mView, R.id.calendar_container, View.GONE); + } mCurrentQuery |= TOKEN_QUERY_DUPLICATE_CALENDARS; } break; case TOKEN_QUERY_DUPLICATE_CALENDARS: - SpannableStringBuilder sb = new SpannableStringBuilder(); - - // Calendar display name - String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); - sb.append(calendarName); - - // Show email account if display name is not unique and - // display name != email - String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); - if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) && - Utils.isValidEmail(email)) { - sb.append(" (").append(email).append(")"); - } + if (!mInNonUiMode) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + + // Calendar display name + String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); + sb.append(calendarName); + + // Show email account if display name is not unique and + // display name != email + String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); + if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) && + Utils.isValidEmail(email)) { + sb.append(" (").append(email).append(")"); + } - setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE); - setTextCommon(mView, R.id.calendar_name, sb); + setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE); + setTextCommon(mView, R.id.calendar_name, sb); + } break; } cursor.close(); - sendAccessibilityEventIfQueryDone(token); + assessQueryCompletion(token); // All queries are done, show the view. - if (mCurrentQuery == TOKEN_QUERY_ALL) { + if (mCurrentQuery == TOKEN_QUERY_ALL && !mInNonUiMode) { if (mLoadingMsgView.getAlpha() == 1) { // Loading message is showing, let it stay a bit more (to prevent // flashing) by adding a start delay to the event animation @@ -648,10 +660,18 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } - private void sendAccessibilityEventIfQueryDone(int token) { + private void assessQueryCompletion(int token) { mCurrentQuery |= token; if (mCurrentQuery == TOKEN_QUERY_ALL) { - sendAccessibilityEvent(); + + // signal query completion + if (mQueryCompleteRunnable != null) { + mActivity.runOnUiThread(mQueryCompleteRunnable); + } + if (!mInNonUiMode) { + sendAccessibilityEvent(); + } + } } @@ -699,6 +719,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mEventId = eventId; } + public void launchInNonUiMode() { + mInNonUiMode = true; + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -739,6 +763,10 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + public void setQueryCompleteRunnable(Runnable runnable) { + mQueryCompleteRunnable = runnable; + } + private void applyDialogParams() { Dialog dialog = getDialog(); dialog.setCanceledOnTouchOutside(true); @@ -870,6 +898,21 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + /** + * Initiate the querying of the calendar event data. + * For when this component is started in a non-ui mode + */ + public void startQueryingData(Context context) { + mContext = context; + if (context instanceof Activity) { + mActivity = (Activity) context; + } + + mHandler = new QueryHandler(context); + mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, + null, null, null); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -1112,7 +1155,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange // Overwrites the one from Event table if available if (!TextUtils.isEmpty(name)) { mEventOrganizerDisplayName = name; - if (!mIsOrganizer) { + if (!mIsOrganizer && view != null) { setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); } @@ -1160,7 +1203,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } while (mAttendeesCursor.moveToNext()); mAttendeesCursor.moveToFirst(); - updateAttendees(view); + if (view != null) updateAttendees(view); } } } @@ -1278,64 +1321,13 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange * Generates an .ics formatted file with the event info and launches intent chooser to * share said file */ - private void shareEvent(ShareType type) { - // Create the respective ICalendar objects from the event info - VCalendar calendar = new VCalendar(); - calendar.addProperty(VCalendar.VERSION, "2.0"); - calendar.addProperty(VCalendar.PRODID, VCalendar.PRODUCT_IDENTIFIER); - calendar.addProperty(VCalendar.CALSCALE, "GREGORIAN"); - calendar.addProperty(VCalendar.METHOD, "REQUEST"); - - VEvent event = new VEvent(); - mEventCursor.moveToFirst(); - // add event start and end datetime - if (!mAllDay) { - String eventTimeZone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); - event.addEventStart(mStartMillis, eventTimeZone); - event.addEventEnd(mEndMillis, eventTimeZone); - } else { - // All-day events' start and end time are stored as UTC. - // Treat the event start and end time as being in the local time zone and convert them - // to the corresponding UTC datetime. If the UTC time is used as is, the ical recipients - // will report the wrong start and end time (+/- 1 day) for the event as they will - // convert the UTC time to their respective local time-zones - String localTimeZone = Utils.getTimeZone(mActivity, mTZUpdater); - long eventStart = IcalendarUtils.convertTimeToUtc(mStartMillis, localTimeZone); - long eventEnd = IcalendarUtils.convertTimeToUtc(mEndMillis, localTimeZone); - event.addEventStart(eventStart, "UTC"); - event.addEventEnd(eventEnd, "UTC"); - } - - event.addProperty(VEvent.LOCATION, mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)); - event.addProperty(VEvent.DESCRIPTION, mEventCursor.getString(EVENT_INDEX_DESCRIPTION)); - event.addProperty(VEvent.SUMMARY, mEventCursor.getString(EVENT_INDEX_TITLE)); - event.addOrganizer(new Organizer(mEventOrganizerDisplayName, mEventOrganizerEmail)); - - // Add Attendees to event - for (Attendee attendee : mAcceptedAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mDeclinedAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mTentativeAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - for (Attendee attendee : mNoResponseAttendees) { - IcalendarUtils.addAttendeeToEvent(attendee, event); - } - - // compose all of the ICalendar objects - calendar.addEvent(event); - + public void shareEvent(ShareType type) { + VCalendar calendar = generateVCalendar(); // create and share ics file boolean isShareSuccessful = false; try { // event title serves as the file name prefix - String filePrefix = event.getProperty(VEvent.SUMMARY); + String filePrefix = calendar.getFirstEvent().getProperty(VEvent.SUMMARY); if (filePrefix == null || filePrefix.length() < 3) { // default to a generic filename if event title doesn't qualify // prefix length constraint is imposed by File#createTempFile @@ -1358,8 +1350,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange dir = mActivity.getExternalCacheDir(); } - File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".ics", - dir); + File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".ics", dir); if (IcalendarUtils.writeCalendarToFile(calendar, inviteFile)) { if (type == ShareType.INTENT) { @@ -1393,9 +1384,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } startActivity(chooserIntent); } else { - String msg = getString(R.string.cal_export_succ_msg); - Toast.makeText(mActivity, String.format(msg, inviteFile), - Toast.LENGTH_SHORT).show(); + if (! mInNonUiMode) { + String msg = getString(R.string.cal_export_succ_msg); + Toast.makeText(mActivity, String.format(msg, inviteFile), + Toast.LENGTH_SHORT).show(); + } } isShareSuccessful = true; @@ -1414,6 +1407,66 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } } + /** + * Creates a calendar object (VCalendar) that adheres to the ICalendar specification + */ + public VCalendar generateVCalendar() { + + // Create the respective ICalendar objects from the event info + VCalendar calendar = new VCalendar(); + calendar.addProperty(VCalendar.VERSION, "2.0"); + calendar.addProperty(VCalendar.PRODID, VCalendar.PRODUCT_IDENTIFIER); + calendar.addProperty(VCalendar.CALSCALE, "GREGORIAN"); + calendar.addProperty(VCalendar.METHOD, "REQUEST"); + + VEvent event = new VEvent(); + mEventCursor.moveToFirst(); + // add event start and end datetime + if (!mAllDay) { + String eventTimeZone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); + event.addEventStart(mStartMillis, eventTimeZone); + event.addEventEnd(mEndMillis, eventTimeZone); + } else { + // All-day events' start and end time are stored as UTC. + // Treat the event start and end time as being in the local time zone and convert them + // to the corresponding UTC datetime. If the UTC time is used as is, the ical recipients + // will report the wrong start and end time (+/- 1 day) for the event as they will + // convert the UTC time to their respective local time-zones + String localTimeZone = Utils.getTimeZone(mActivity, mTZUpdater); + long eventStart = IcalendarUtils.convertTimeToUtc(mStartMillis, localTimeZone); + long eventEnd = IcalendarUtils.convertTimeToUtc(mEndMillis, localTimeZone); + event.addEventStart(eventStart, "UTC"); + event.addEventEnd(eventEnd, "UTC"); + } + + event.addProperty(VEvent.LOCATION, mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)); + event.addProperty(VEvent.DESCRIPTION, mEventCursor.getString(EVENT_INDEX_DESCRIPTION)); + event.addProperty(VEvent.SUMMARY, mEventCursor.getString(EVENT_INDEX_TITLE)); + event.addOrganizer(new Organizer(mEventOrganizerDisplayName, mEventOrganizerEmail)); + + // Add Attendees to event + for (Attendee attendee : mAcceptedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mDeclinedAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mTentativeAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + for (Attendee attendee : mNoResponseAttendees) { + IcalendarUtils.addAttendeeToEvent(attendee, event); + } + + // compose all of the ICalendar objects + calendar.addEvent(event); + + return calendar; + } + private void showEventColorPickerDialog() { if (mColorPickerDialog == null) { mColorPickerDialog = EventColorPickerDialog.newInstance(mColors, mCurrentColor, @@ -1925,12 +1978,15 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mEventOrganizerDisplayName = mEventOrganizerEmail; } - if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) { - setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); - setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); - } else { - setVisibilityCommon(view, R.id.organizer_container, View.GONE); + if (!mInNonUiMode) { + if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) { + setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName); + setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); + } else { + setVisibilityCommon(view, R.id.organizer_container, View.GONE); + } } + mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CAL_ACCESS_CONTRIBUTOR; @@ -1939,6 +1995,9 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mIsBusyFreeCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; + // no more work to be done if launched in non-ui mode + if (mInNonUiMode) return; + if (!mIsBusyFreeCalendar) { View b = mView.findViewById(R.id.edit); @@ -1978,8 +2037,8 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mActivity.invalidateOptionsMenu(); } } else { - setVisibilityCommon(view, R.id.calendar, View.GONE); - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); + if (!mInNonUiMode) setVisibilityCommon(view, R.id.calendar, View.GONE); + assessQueryCompletion(TOKEN_QUERY_DUPLICATE_CALENDARS); } } diff --git a/src/com/android/calendar/ShareCalendarActivity.java b/src/com/android/calendar/ShareCalendarActivity.java new file mode 100644 index 00000000..cd538ca6 --- /dev/null +++ b/src/com/android/calendar/ShareCalendarActivity.java @@ -0,0 +1,229 @@ +package com.android.calendar; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import com.android.calendar.agenda.AgendaFragment; +import com.android.calendar.icalendar.IcalendarUtils; +import com.android.calendar.icalendar.VCalendar; +import com.android.calendar.icalendar.VEvent; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Handles requests for sharing calendar events + * This activity returns a vcs formatted file + */ +public class ShareCalendarActivity extends Activity implements CalendarUtils.ShareEventListener { + + public static final String EXTRA_LIMIT_TO_ONE_EVENT = "EXTRA_LIMIT_TO_ONE_EVENT"; + + private static final String TAG = "ShareCalendarActivity"; + + private HashSet<CalendarUtils.Triple<Long, Long, Long>> mShareEventsList = + new HashSet<CalendarUtils.Triple<Long, Long, Long>>(); + + private ArrayList<EventInfoFragment> mEventDataFragments = new ArrayList<EventInfoFragment>(); + + private ActionBar mActionBar; + private AgendaFragment mAgendaFragment; + private long mStartMillis; + private ArrayList<Uri> mFileUris = new ArrayList<Uri>(); + private int mNumQueriesCompleted; + private boolean mShouldSelectSingleEvent; + private Resources mResources; + private String mNoSelectionMsg; + + @Override + public void onEventShared(CalendarUtils.Triple<Long, Long, Long> eventInfo) { + if (eventInfo.first < 0) return; + + mShareEventsList.add(eventInfo); + updateSubtitle(); + } + + @Override + public void onEventRemoval(long eventId) { + if (eventId < 0) return; + + CalendarUtils.Triple<Long, Long, Long> eventInfo = + new CalendarUtils.Triple<Long, Long, Long>(eventId, 0l, 0l); + mShareEventsList.remove(eventInfo); + updateSubtitle(); + } + + @Override + public void onResetShareList() { + // undo + mShareEventsList.clear(); + updateSubtitle(); + } + + public void updateSubtitle() { + int listSize = mShareEventsList.size(); + String subtitle; + // workaround for Android's poor pluralization treatment of 'zero' quantity in English + if (listSize == 0) { + subtitle = mNoSelectionMsg; + } else { + subtitle = mResources.getQuantityString(R.plurals.events_selected, + listSize, listSize); + } + mActionBar.setSubtitle(subtitle); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Bundle args = intent.getExtras(); + if (args != null) { + mShouldSelectSingleEvent = args.getBoolean(EXTRA_LIMIT_TO_ONE_EVENT, false); + } + + mStartMillis = System.currentTimeMillis(); + setContentView(R.layout.simple_frame_layout); + mResources = getResources(); + + // create agenda fragment displaying the list of calendar events + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction ft = fragmentManager.beginTransaction(); + mAgendaFragment = new AgendaFragment(mStartMillis, false, true); + mAgendaFragment.setShareModeOptions(this, mShouldSelectSingleEvent); + ft.replace(R.id.main_frame, mAgendaFragment); + ft.commit(); + + mActionBar = getActionBar(); + mActionBar.setDisplayShowTitleEnabled(true); + String title = mShouldSelectSingleEvent ? + mResources.getQuantityString(R.plurals.select_events_to_share, 1) : + mResources.getQuantityString(R.plurals.select_events_to_share, 2) ; + + mActionBar.setTitle(title); + mNoSelectionMsg = mResources.getString(R.string.no_events_selected); + updateSubtitle(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.share_event_title_bar, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case(R.id.action_done): + generateEventData(); + evalIfComplete(); + break; + + case (R.id.action_cancel): + setResultAndFinish(false); + break; + + } + return super.onOptionsItemSelected(item); + } + + // called after the data fragment's is done loading + public void queryComplete() { + mNumQueriesCompleted++; + evalIfComplete(); + } + + // used load event information for the list of selected events + private void generateEventData() { + for (CalendarUtils.Triple<Long, Long, Long> event : mShareEventsList) { + + long eventId = event.first; + long eventStartMillis = event.second; + long eventEndMillis = event.third; + + // EventInfoFragment just serves as a data fragment and is initialized with + // default arguments for parameters that don't affect model loading + EventInfoFragment eif = new EventInfoFragment(this, eventId, eventStartMillis, + eventEndMillis, CalendarContract.Attendees.ATTENDEE_STATUS_NONE, false, 0, + null); + eif.launchInNonUiMode(); + eif.startQueryingData(this); + eif.setQueryCompleteRunnable(new Runnable() { + @Override + public void run() { + // indicate model loading is complete + queryComplete(); + } + }); + + mEventDataFragments.add(eif); + } + + } + + // generates the vcs files if the data for selected events has been successfully queried + private void evalIfComplete() { + if (mNumQueriesCompleted != 0 && mNumQueriesCompleted == mEventDataFragments.size()) { + + for(EventInfoFragment event : mEventDataFragments) { + try { + // generate vcs file + VCalendar calendar = event.generateVCalendar(); + // event title serves as the file name prefix + String filePrefix = calendar.getFirstEvent().getProperty(VEvent.SUMMARY); + if (filePrefix == null || filePrefix.length() < 3) { + // default to a generic filename if event title doesn't qualify + // prefix length constraint is imposed by File#createTempFile + filePrefix = "invite"; + } + + filePrefix = filePrefix.replaceAll("\\W+", " "); + + if (!filePrefix.endsWith(" ")) { + filePrefix += " "; + } + File dir = getExternalCacheDir(); + File inviteFile = IcalendarUtils.createTempFile(filePrefix, ".vcs", dir); + inviteFile.setReadable(true, false); // set world-readable + if (IcalendarUtils.writeCalendarToFile(calendar, inviteFile)) { + mFileUris.add(Uri.fromFile(inviteFile)); + } + } catch (IOException ioe) { + break; + } + } + + setResultAndFinish(true); + + } else if (mEventDataFragments.size() < 1) { // if no events have been selected + setResultAndFinish(false); + } + } + + private void setResultAndFinish(boolean isResultAvailable) { + if (isResultAvailable) { + Intent intent = new Intent(); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mFileUris); + setResult(RESULT_OK, intent); + finish(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } +} diff --git a/src/com/android/calendar/agenda/AgendaAdapter.java b/src/com/android/calendar/agenda/AgendaAdapter.java index 9e492832..0b42bb9d 100644 --- a/src/com/android/calendar/agenda/AgendaAdapter.java +++ b/src/com/android/calendar/agenda/AgendaAdapter.java @@ -26,6 +26,7 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.ResourceCursorAdapter; import android.widget.TextView; @@ -78,6 +79,7 @@ public class AgendaAdapter extends ResourceCursorAdapter { boolean allDay; boolean grayed; int julianDay; + CheckBox selectedForSharing; } public AgendaAdapter(Context context, int resource) { @@ -125,6 +127,7 @@ public class AgendaAdapter extends ResourceCursorAdapter { view.findViewById(R.id.agenda_item_text_container); holder.selectedMarker = view.findViewById(R.id.selected_marker); holder.colorChip = (ColorChipView)view.findViewById(R.id.agenda_item_color); + holder.selectedForSharing = (CheckBox) view.findViewById(R.id.shareCheckbox); } holder.startTimeMilli = cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN); diff --git a/src/com/android/calendar/agenda/AgendaFragment.java b/src/com/android/calendar/agenda/AgendaFragment.java index ff5c47cc..6328bd63 100644 --- a/src/com/android/calendar/agenda/AgendaFragment.java +++ b/src/com/android/calendar/agenda/AgendaFragment.java @@ -41,6 +41,7 @@ import com.android.calendar.CalendarController.ViewType; import com.android.calendar.EventInfoFragment; import com.android.calendar.GeneralPreferences; import com.android.calendar.R; +import com.android.calendar.CalendarUtils.ShareEventListener; import com.android.calendar.StickyHeaderListView; import com.android.calendar.Utils; @@ -71,8 +72,9 @@ public class AgendaFragment extends Fragment implements CalendarController.Event private AgendaWindowAdapter mAdapter = null; private boolean mForceReplace = true; private long mLastShownEventId = -1; - - + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private ShareEventListener mShareEventListener; // Tracks the time of the top visible view in order to send UPDATE_TITLE messages to the action // bar. @@ -87,13 +89,17 @@ public class AgendaFragment extends Fragment implements CalendarController.Event }; public AgendaFragment() { - this(0, false); + this(0, false, false); } + public AgendaFragment(long timeMillis, boolean usedForSearch) { + this(0, usedForSearch, false); + } // timeMillis - time of first event to show // usedForSearch - indicates if this fragment is used in the search fragment - public AgendaFragment(long timeMillis, boolean usedForSearch) { + // inShareMode - indicates whether the fragment was started to share calendar events + public AgendaFragment(long timeMillis, boolean usedForSearch, boolean inShareMode) { mInitialTimeMillis = timeMillis; mTime = new Time(); mLastHandledEventTime = new Time(); @@ -105,6 +111,7 @@ public class AgendaFragment extends Fragment implements CalendarController.Event } mLastHandledEventTime.set(mTime); mUsedForSearch = usedForSearch; + mLaunchedInShareMode = inShareMode; } @Override @@ -142,7 +149,6 @@ public class AgendaFragment extends Fragment implements CalendarController.Event public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - int screenWidth = mActivity.getResources().getDisplayMetrics().widthPixels; View v = inflater.inflate(R.layout.agenda_fragment, null); @@ -168,14 +174,24 @@ public class AgendaFragment extends Fragment implements CalendarController.Event if (lv != null) { Adapter a = mAgendaListView.getAdapter(); lv.setAdapter(a); + if (a instanceof HeaderViewListAdapter) { mAdapter = (AgendaWindowAdapter) ((HeaderViewListAdapter)a).getWrappedAdapter(); + if (mLaunchedInShareMode) { + mAdapter.launchInShareMode(true, mShouldSelectSingleEvent); + mAgendaListView.launchInShareMode(true, mShouldSelectSingleEvent); + if (mShareEventListener != null) { + mAgendaListView.setShareEventListener(mShareEventListener); + } + } lv.setIndexer(mAdapter); lv.setHeaderHeightListener(mAdapter); + } else if (a instanceof AgendaWindowAdapter) { mAdapter = (AgendaWindowAdapter)a; lv.setIndexer(mAdapter); lv.setHeaderHeightListener(mAdapter); + } else { Log.wtf(TAG, "Cannot find HeaderIndexer for StickyHeaderListView"); } @@ -208,6 +224,13 @@ public class AgendaFragment extends Fragment implements CalendarController.Event return v; } + // configure share mode launch options + public void setShareModeOptions(ShareEventListener listener, boolean selectSingleEvent) { + mShareEventListener = listener; + mShouldSelectSingleEvent = selectSingleEvent; + + } + @Override public void onResume() { super.onResume(); diff --git a/src/com/android/calendar/agenda/AgendaListView.java b/src/com/android/calendar/agenda/AgendaListView.java index 6cfc7e5b..f3270e66 100644 --- a/src/com/android/calendar/agenda/AgendaListView.java +++ b/src/com/android/calendar/agenda/AgendaListView.java @@ -20,6 +20,8 @@ import com.android.calendar.CalendarController; import com.android.calendar.CalendarController.EventType; import com.android.calendar.DeleteEventHelper; import com.android.calendar.R; +import com.android.calendar.CalendarUtils.ShareEventListener; +import com.android.calendar.CalendarUtils.Triple; import com.android.calendar.Utils; import com.android.calendar.agenda.AgendaAdapter.ViewHolder; import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo; @@ -51,6 +53,9 @@ public class AgendaListView extends ListView implements OnItemClickListener { private Time mTime; private boolean mShowEventDetailsWithAgenda; private Handler mHandler = null; + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private ShareEventListener mShareEventListener; private final Runnable mTZUpdater = new Runnable() { @Override @@ -86,6 +91,12 @@ public class AgendaListView extends ListView implements OnItemClickListener { initView(context); } + // "share mode" is off by default + public void launchInShareMode(boolean inShareMode, boolean selectSingleEvent) { + mLaunchedInShareMode = true; + mShouldSelectSingleEvent = selectSingleEvent; + } + private void initView(Context context) { mContext = context; mTimeZone = Utils.getTimeZone(context, mTZUpdater); @@ -108,6 +119,10 @@ public class AgendaListView extends ListView implements OnItemClickListener { mHandler = new Handler(); } + public void setShareEventListener(ShareEventListener listener) { + mShareEventListener = listener; + } + // Sets a thread to run every EVENT_UPDATE_TIME in order to update the list // with grayed out past events private void setPastEventsUpdater() { @@ -197,10 +212,30 @@ public class AgendaListView extends ListView implements OnItemClickListener { endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); } mTime.set(startTime); - CalendarController controller = CalendarController.getInstance(mContext); - controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id, - startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( - Attendees.ATTENDEE_STATUS_NONE, item.allDay), holderStartTime); + + // divert click action to either a ShareEventListener or the Controller + if (mShareEventListener != null && mLaunchedInShareMode) { + long viewId = ((ViewHolder) holder).instanceId; + + if (mWindowAdapter.isEventInShareList(viewId)) { + Triple<Long, Long, Long> eventinfo = + new Triple<Long, Long, Long>(item.id, item.begin, item.end); + if (mShouldSelectSingleEvent) mShareEventListener.onResetShareList(); + mShareEventListener.onEventShared(eventinfo); + + } else { + // submit event removal + mShareEventListener.onEventRemoval(item.id); + } + + } else { + CalendarController controller = CalendarController.getInstance(mContext); + controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id, + startTime, endTime, 0, 0, + CalendarController.EventInfo.buildViewExtraLong( + Attendees.ATTENDEE_STATUS_NONE, item.allDay), + holderStartTime); + } } } } diff --git a/src/com/android/calendar/agenda/AgendaWindowAdapter.java b/src/com/android/calendar/agenda/AgendaWindowAdapter.java index 9fd59f0f..97822f98 100644 --- a/src/com/android/calendar/agenda/AgendaWindowAdapter.java +++ b/src/com/android/calendar/agenda/AgendaWindowAdapter.java @@ -50,6 +50,7 @@ import com.android.calendar.Utils; import java.util.Date; import java.util.Formatter; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Locale; @@ -74,7 +75,7 @@ Check for leaks and excessive allocations */ public class AgendaWindowAdapter extends BaseAdapter - implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener{ + implements StickyHeaderListView.HeaderIndexer, StickyHeaderListView.HeaderHeightListener { static final boolean BASICLOG = false; static final boolean DEBUGLOG = false; @@ -228,6 +229,10 @@ public class AgendaWindowAdapter extends BaseAdapter private final int mSelectedItemTextColor; private final float mItemRightMargin; + private boolean mLaunchedInShareMode; + private boolean mShouldSelectSingleEvent; + private HashSet<Long> mSharedEvents = new HashSet<Long>(); + // Types of Query private static final int QUERY_TYPE_OLDER = 0; // Query for older events private static final int QUERY_TYPE_NEWER = 1; // Query for newer events @@ -369,6 +374,12 @@ public class AgendaWindowAdapter extends BaseAdapter mAgendaListView.addHeaderView(mHeaderView); } + // "share mode" is off by default + public void launchInShareMode(boolean inShareMode, boolean selectSingleEvent) { + mLaunchedInShareMode = inShareMode; + mShouldSelectSingleEvent = selectSingleEvent; + } + // Method in Adapter @Override public int getViewTypeCount() { @@ -485,6 +496,25 @@ public class AgendaWindowAdapter extends BaseAdapter pastPresentDivider.setVisibility(View.GONE); } } + + if (mLaunchedInShareMode) { + Object tag = v.getTag(); + if (tag instanceof AgendaAdapter.ViewHolder) { + AgendaAdapter.ViewHolder vh = (AgendaAdapter.ViewHolder) tag; + + // toggle visibility of share checkbox + vh.selectedForSharing.setVisibility(View.VISIBLE); + vh.selectedForSharing.setClickable(false); + + // set 'checked' status + if (mSharedEvents.contains(vh.instanceId)) { + vh.selectedForSharing.setChecked(true); + } else { + vh.selectedForSharing.setChecked(false); + } + } + } + } else { // TODO Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position); @@ -1310,10 +1340,32 @@ public class AgendaWindowAdapter extends BaseAdapter Object vh = v.getTag(); if (vh instanceof AgendaAdapter.ViewHolder) { mSelectedVH = (AgendaAdapter.ViewHolder) vh; + boolean datasetChanged = false; + + if (mLaunchedInShareMode) { + + if (mShouldSelectSingleEvent) { + mSharedEvents.clear(); + mSharedEvents.add(mSelectedVH.instanceId); + } else { + + if (mSharedEvents.contains(mSelectedVH.instanceId)) { + mSharedEvents.remove(mSelectedVH.instanceId); + } else { + mSharedEvents.add(mSelectedVH.instanceId); + } + + } + + datasetChanged = true; + } + if (mSelectedInstanceId != mSelectedVH.instanceId) { mSelectedInstanceId = mSelectedVH.instanceId; - notifyDataSetChanged(); + datasetChanged = true; } + + if (datasetChanged) notifyDataSetChanged(); } } } @@ -1397,6 +1449,10 @@ public class AgendaWindowAdapter extends BaseAdapter return -1; } + public boolean isEventInShareList(long id) { + return mSharedEvents.contains(id); + } + @Override public void OnHeaderHeightChanged(int height) { mStickyHeaderSize = height; diff --git a/src/com/android/calendar/icalendar/VCalendar.java b/src/com/android/calendar/icalendar/VCalendar.java index 9f422e6f..140d9358 100644 --- a/src/com/android/calendar/icalendar/VCalendar.java +++ b/src/com/android/calendar/icalendar/VCalendar.java @@ -52,11 +52,11 @@ public class VCalendar { * Add specified property * @param property * @param value - * @return */ public boolean addProperty(String property, String value) { - // since all the required mProperties are unary (only one can exist) , taking a shortcut here - // when multiples of a property can exist , enforce that here .. cleverly + // since all the required mProperties are unary (only one can exist) , taking a shortcut + // here + // TODO: when multiple attributes of a property can exist , enforce that here if (sPropertyList.containsKey(property) && value != null) { mProperties.put(property, IcalendarUtils.cleanseString(value)); return true; @@ -73,16 +73,25 @@ public class VCalendar { } /** - * - * @return + * Returns all the events that are part of this calendar */ public LinkedList<VEvent> getAllEvents() { return mEvents; } /** + * Returns the first event of the calendar + */ + public VEvent getFirstEvent() { + if (mEvents != null && mEvents.size() > 0) { + return mEvents.get(0); + } else { + return null; + } + } + + /** * Returns the iCal representation of the calendar and all of its inherent components - * @return */ public String getICalFormattedString() { StringBuilder output = new StringBuilder(); |