diff options
author | Steve Kondik <shade@chemlab.org> | 2012-11-18 22:37:21 -0800 |
---|---|---|
committer | Steve Kondik <shade@chemlab.org> | 2012-11-18 22:37:21 -0800 |
commit | 3905962fd7d1cf204d8fb5ea3c7c709814faefc7 (patch) | |
tree | 5593373426fa3806f4ebf5d4fbeb7ed47d484c45 /src | |
parent | ed8b229028feab713d38d47023e5d5729d456fcc (diff) | |
parent | 898d0e6ac26d5fbcbe8a57d46acea4200ae1aeac (diff) | |
download | android_packages_apps_Calendar-3905962fd7d1cf204d8fb5ea3c7c709814faefc7.tar.gz android_packages_apps_Calendar-3905962fd7d1cf204d8fb5ea3c7c709814faefc7.tar.bz2 android_packages_apps_Calendar-3905962fd7d1cf204d8fb5ea3c7c709814faefc7.zip |
Merge branch 'jb-mr1-release' of https://android.googlesource.com/platform/packages/apps/Calendar into HEADcm-10.1-M1
Change-Id: I7d1aa6b528efe2d7df3abb3cb884c25df61037ab
Diffstat (limited to 'src')
35 files changed, 1199 insertions, 592 deletions
diff --git a/src/com/android/calendar/AboutPreferences.java b/src/com/android/calendar/AboutPreferences.java deleted file mode 100644 index 37dd5e3e..00000000 --- a/src/com/android/calendar/AboutPreferences.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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. - */ - -package com.android.calendar; - -import android.app.Activity; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import android.preference.PreferenceFragment; - -public class AboutPreferences extends PreferenceFragment { - private static final String BUILD_VERSION = "build_version"; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.about_preferences); - - final Activity activity = getActivity(); - try { - final PackageInfo packageInfo = - activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0); - findPreference(BUILD_VERSION).setSummary(packageInfo.versionName); - } catch (NameNotFoundException e) { - findPreference(BUILD_VERSION).setSummary("?"); - } - } -}
\ No newline at end of file diff --git a/src/com/android/calendar/AllInOneActivity.java b/src/com/android/calendar/AllInOneActivity.java index 278dbc72..d86ef86f 100644 --- a/src/com/android/calendar/AllInOneActivity.java +++ b/src/com/android/calendar/AllInOneActivity.java @@ -39,9 +39,7 @@ import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentUris; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Configuration; @@ -77,6 +75,7 @@ import com.android.calendar.CalendarController.EventInfo; import com.android.calendar.CalendarController.EventType; import com.android.calendar.CalendarController.ViewType; import com.android.calendar.agenda.AgendaFragment; +import com.android.calendar.extensions.AllInOneMenuExtensions; import com.android.calendar.month.MonthByWeekFragment; import com.android.calendar.selectcalendars.SelectVisibleCalendarsFragment; @@ -168,6 +167,8 @@ public class AllInOneActivity extends Activity implements EventHandler, private LayoutParams mControlsParams; private LinearLayout.LayoutParams mVerticalControlsParams; + private AllInOneMenuExtensions mExtensions = new AllInOneMenuExtensions(); + private final AnimatorListener mSlideAnimationDoneListener = new AnimatorListener() { @Override @@ -615,11 +616,16 @@ public class AllInOneActivity extends Activity implements EventHandler, public void onSaveInstanceState(Bundle outState) { mOnSaveInstanceStateCalled = true; super.onSaveInstanceState(outState); - outState.putLong(BUNDLE_KEY_RESTORE_TIME, mController.getTime()); outState.putInt(BUNDLE_KEY_RESTORE_VIEW, mCurrentView); if (mCurrentView == ViewType.EDIT) { outState.putLong(BUNDLE_KEY_EVENT_ID, mController.getEventId()); + } else if (mCurrentView == ViewType.AGENDA) { + FragmentManager fm = getFragmentManager(); + Fragment f = fm.findFragmentById(R.id.main_pane); + if (f instanceof AgendaFragment) { + outState.putLong(BUNDLE_KEY_EVENT_ID, ((AgendaFragment)f).getLastShowEventId()); + } } outState.putBoolean(BUNDLE_KEY_CHECK_ACCOUNTS, mCheckForAccounts); } @@ -703,7 +709,10 @@ public class AllInOneActivity extends Activity implements EventHandler, Time t = new Time(mTimeZone); t.set(timeMillis); - if (viewType != ViewType.EDIT) { + if (viewType == ViewType.AGENDA && icicle != null) { + mController.sendEvent(this, EventType.GO_TO, t, null, + icicle.getLong(BUNDLE_KEY_EVENT_ID, -1), viewType); + } else if (viewType != ViewType.EDIT) { mController.sendEvent(this, EventType.GO_TO, t, null, -1, viewType); } } @@ -723,6 +732,12 @@ public class AllInOneActivity extends Activity implements EventHandler, mOptionsMenu = menu; getMenuInflater().inflate(R.menu.all_in_one_title_bar, menu); + // Add additional options (if any). + Integer extensionMenuRes = mExtensions.getExtensionMenuResource(menu); + if (extensionMenuRes != null) { + getMenuInflater().inflate(extensionMenuRes, menu); + } + mSearchMenu = menu.findItem(R.id.action_search); mSearchView = (SearchView) mSearchMenu.getActionView(); if (mSearchView != null) { @@ -749,10 +764,15 @@ public class AllInOneActivity extends Activity implements EventHandler, mControlsMenu.setTitle(mHideControls ? mShowString : mHideString); } - // replace the default top layer drawable of the today icon with a custom drawable - // that shows the day of the month of today - LayerDrawable icon = (LayerDrawable)menu.findItem(R.id.action_today).getIcon(); - Utils.setTodayIcon(icon, this, mTimeZone); + MenuItem menuItem = menu.findItem(R.id.action_today); + if (Utils.isJellybeanOrLater()) { + // replace the default top layer drawable of the today icon with a + // custom drawable that shows the day of the month of today + LayerDrawable icon = (LayerDrawable) menuItem.getIcon(); + Utils.setTodayIcon(icon, this, mTimeZone); + } else { + menuItem.setIcon(R.drawable.ic_menu_today_no_date_holo_light); + } return true; } @@ -761,56 +781,55 @@ public class AllInOneActivity extends Activity implements EventHandler, Time t = null; int viewType = ViewType.CURRENT; long extras = CalendarController.EXTRA_GOTO_TIME; - switch (item.getItemId()) { - case R.id.action_refresh: - mController.refreshCalendars(); - return true; - case R.id.action_today: - viewType = ViewType.CURRENT; - t = new Time(mTimeZone); - t.setToNow(); - extras |= CalendarController.EXTRA_GOTO_TODAY; - break; - case R.id.action_create_event: - t = new Time(); - t.set(mController.getTime()); - if (t.minute > 30) { - t.hour++; - t.minute = 0; - } else if (t.minute > 0 && t.minute < 30) { - t.minute = 30; - } - mController.sendEventRelatedEvent( - this, EventType.CREATE_EVENT, -1, t.toMillis(true), 0, 0, 0, -1); - return true; - case R.id.action_select_visible_calendars: - mController.sendEvent(this, EventType.LAUNCH_SELECT_VISIBLE_CALENDARS, null, null, - 0, 0); - return true; - case R.id.action_settings: - mController.sendEvent(this, EventType.LAUNCH_SETTINGS, null, null, 0, 0); - return true; - case R.id.action_hide_controls: - mHideControls = !mHideControls; - Utils.setSharedPreference( - this, GeneralPreferences.KEY_SHOW_CONTROLS, !mHideControls); - item.setTitle(mHideControls ? mShowString : mHideString); - if (!mHideControls) { - mMiniMonth.setVisibility(View.VISIBLE); - mCalendarsList.setVisibility(View.VISIBLE); - mMiniMonthContainer.setVisibility(View.VISIBLE); - } - final ObjectAnimator slideAnimation = ObjectAnimator.ofInt(this, "controlsOffset", - mHideControls ? 0 : mControlsAnimateWidth, - mHideControls ? mControlsAnimateWidth : 0); - slideAnimation.setDuration(mCalendarControlsAnimationTime); - ObjectAnimator.setFrameDelay(0); - slideAnimation.start(); - return true; - case R.id.action_search: - return false; - default: - return false; + final int itemId = item.getItemId(); + if (itemId == R.id.action_refresh) { + mController.refreshCalendars(); + return true; + } else if (itemId == R.id.action_today) { + viewType = ViewType.CURRENT; + t = new Time(mTimeZone); + t.setToNow(); + extras |= CalendarController.EXTRA_GOTO_TODAY; + } else if (itemId == R.id.action_create_event) { + t = new Time(); + t.set(mController.getTime()); + if (t.minute > 30) { + t.hour++; + t.minute = 0; + } else if (t.minute > 0 && t.minute < 30) { + t.minute = 30; + } + mController.sendEventRelatedEvent( + this, EventType.CREATE_EVENT, -1, t.toMillis(true), 0, 0, 0, -1); + return true; + } else if (itemId == R.id.action_select_visible_calendars) { + mController.sendEvent(this, EventType.LAUNCH_SELECT_VISIBLE_CALENDARS, null, null, + 0, 0); + return true; + } else if (itemId == R.id.action_settings) { + mController.sendEvent(this, EventType.LAUNCH_SETTINGS, null, null, 0, 0); + return true; + } else if (itemId == R.id.action_hide_controls) { + mHideControls = !mHideControls; + Utils.setSharedPreference( + this, GeneralPreferences.KEY_SHOW_CONTROLS, !mHideControls); + item.setTitle(mHideControls ? mShowString : mHideString); + if (!mHideControls) { + mMiniMonth.setVisibility(View.VISIBLE); + mCalendarsList.setVisibility(View.VISIBLE); + mMiniMonthContainer.setVisibility(View.VISIBLE); + } + final ObjectAnimator slideAnimation = ObjectAnimator.ofInt(this, "controlsOffset", + mHideControls ? 0 : mControlsAnimateWidth, + mHideControls ? mControlsAnimateWidth : 0); + slideAnimation.setDuration(mCalendarControlsAnimationTime); + ObjectAnimator.setFrameDelay(0); + slideAnimation.start(); + return true; + } else if (itemId == R.id.action_search) { + return false; + } else { + return mExtensions.handleItemSelected(item, this); } mController.sendEvent(this, EventType.GO_TO, t, null, t, -1, viewType, extras, null, null); return true; @@ -1165,7 +1184,8 @@ public class AllInOneActivity extends Activity implements EventHandler, event.endTime, event.endTime.toMillis(false), mTimeZone); } mController.sendEvent(this, EventType.GO_TO, event.startTime, event.endTime, - event.id, ViewType.AGENDA); + event.selectedTime, event.id, ViewType.AGENDA, + CalendarController.EXTRA_GOTO_TIME, null, null); } else if (event.selectedTime != null) { mController.sendEvent(this, EventType.GO_TO, event.selectedTime, event.selectedTime, event.id, ViewType.AGENDA); diff --git a/src/com/android/calendar/CalendarApplication.java b/src/com/android/calendar/CalendarApplication.java index d0ca4698..dc08bb77 100644 --- a/src/com/android/calendar/CalendarApplication.java +++ b/src/com/android/calendar/CalendarApplication.java @@ -28,5 +28,10 @@ public class CalendarApplication extends Application { * service, etc. of Calendar */ GeneralPreferences.setDefaultValues(this); + + // Save the version number, for upcoming 'What's new' screen. This will be later be + // moved to that implementation. + Utils.setSharedPreference(this, GeneralPreferences.KEY_VERSION, + Utils.getVersionCode(this)); } } diff --git a/src/com/android/calendar/CalendarSettingsActivity.java b/src/com/android/calendar/CalendarSettingsActivity.java index 97176485..0a1b90e5 100644 --- a/src/com/android/calendar/CalendarSettingsActivity.java +++ b/src/com/android/calendar/CalendarSettingsActivity.java @@ -72,17 +72,16 @@ public class CalendarSettingsActivity extends PreferenceActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - case R.id.action_add_account: - Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT); - final String[] array = { "com.android.calendar" }; - nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array); - nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(nextIntent); - return true; + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_add_account) { + Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT); + final String[] array = { "com.android.calendar" }; + nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array); + nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(nextIntent); + return true; } return super.onOptionsItemSelected(item); } diff --git a/src/com/android/calendar/DayView.java b/src/com/android/calendar/DayView.java index cd32f354..d636b8ab 100644 --- a/src/com/android/calendar/DayView.java +++ b/src/com/android/calendar/DayView.java @@ -471,6 +471,7 @@ public class DayView extends View implements View.OnCreateContextMenuListener, private static int mMinCellHeight = 32; private int mScrollStartY; private int mPreviousDirection; + private static int mScaledPagingTouchSlop = 0; /** * Vertical distance or span between the two touch points at the start of a @@ -768,6 +769,7 @@ public class DayView extends View implements View.OnCreateContextMenuListener, mEdgeEffectTop = new EdgeEffect(context); mEdgeEffectBottom = new EdgeEffect(context); ViewConfiguration vc = ViewConfiguration.get(context); + mScaledPagingTouchSlop = vc.getScaledPagingTouchSlop(); mOnDownDelay = ViewConfiguration.getTapTimeout(); OVERFLING_DISTANCE = vc.getScaledOverflingDistance(); @@ -3990,9 +3992,12 @@ public class DayView extends View implements View.OnCreateContextMenuListener, mPreviousDirection = 0; if (absDistanceX > absDistanceY) { - mTouchMode = TOUCH_MODE_HSCROLL; - mViewStartX = distanceX; - initNextView(-mViewStartX); + int slopFactor = mScaleGestureDetector.isInProgress() ? 20 : 2; + if (absDistanceX > mScaledPagingTouchSlop * slopFactor) { + mTouchMode = TOUCH_MODE_HSCROLL; + mViewStartX = distanceX; + initNextView(-mViewStartX); + } } else { mTouchMode = TOUCH_MODE_VSCROLL; } diff --git a/src/com/android/calendar/DeleteEventHelper.java b/src/com/android/calendar/DeleteEventHelper.java index 0f54c2d3..c436f5a1 100644 --- a/src/com/android/calendar/DeleteEventHelper.java +++ b/src/com/android/calendar/DeleteEventHelper.java @@ -17,7 +17,7 @@ package com.android.calendar; import com.android.calendar.event.EditEventHelper; -import com.android.calendarcommon.EventRecurrence; +import com.android.calendarcommon2.EventRecurrence; import android.app.Activity; import android.app.AlertDialog; diff --git a/src/com/android/calendar/Event.java b/src/com/android/calendar/Event.java index ac07658d..095e43e7 100644 --- a/src/com/android/calendar/Event.java +++ b/src/com/android/calendar/Event.java @@ -67,7 +67,7 @@ public class Event implements Cloneable { Instances.TITLE, // 0 Instances.EVENT_LOCATION, // 1 Instances.ALL_DAY, // 2 - Instances.DISPLAY_COLOR, // 3 + Instances.DISPLAY_COLOR, // 3 If SDK < 16, set to Instances.CALENDAR_COLOR. Instances.EVENT_TIMEZONE, // 4 Instances.EVENT_ID, // 5 Instances.BEGIN, // 6 @@ -108,6 +108,12 @@ public class Event implements Cloneable { private static final int PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX = 18; private static final int PROJECTION_DISPLAY_AS_ALLDAY = 19; + static { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[PROJECTION_COLOR_INDEX] = Instances.CALENDAR_COLOR; + } + } + private static String mNoTitleString; private static int mNoColorColor; diff --git a/src/com/android/calendar/EventInfoActivity.java b/src/com/android/calendar/EventInfoActivity.java index 1cf9f504..2b4c8a13 100644 --- a/src/com/android/calendar/EventInfoActivity.java +++ b/src/com/android/calendar/EventInfoActivity.java @@ -29,6 +29,9 @@ import android.net.Uri; import android.os.Bundle; import android.provider.CalendarContract.Attendees; import android.util.Log; +import android.widget.Toast; + +import java.util.List; public class EventInfoActivity extends Activity { // implements CalendarController.EventHandler, SearchView.OnQueryTextListener, @@ -46,7 +49,7 @@ public class EventInfoActivity extends Activity { // Get the info needed for the fragment Intent intent = getIntent(); int attendeeResponse = 0; - mEventId = 0; + mEventId = -1; boolean isDialog = false; if (icicle != null) { @@ -63,13 +66,38 @@ public class EventInfoActivity extends Activity { Uri data = intent.getData(); if (data != null) { try { - mEventId = Long.parseLong(data.getLastPathSegment()); + List<String> pathSegments = data.getPathSegments(); + int size = pathSegments.size(); + if (size > 2 && "EventTime".equals(pathSegments.get(2))) { + // Support non-standard VIEW intent format: + //dat = content://com.android.calendar/events/[id]/EventTime/[start]/[end] + mEventId = Long.parseLong(pathSegments.get(1)); + if (size > 4) { + mStartMillis = Long.parseLong(pathSegments.get(3)); + mEndMillis = Long.parseLong(pathSegments.get(4)); + } + } else { + mEventId = Long.parseLong(data.getLastPathSegment()); + } } catch (NumberFormatException e) { - Log.wtf(TAG,"No event id"); + if (mEventId == -1) { + // do nothing here , deal with it later + } else if (mStartMillis == 0 || mEndMillis ==0) { + // Parsing failed on the start or end time , make sure the times were not + // pulled from the intent's extras and reset them. + mStartMillis = 0; + mEndMillis = 0; + } } } } + if (mEventId == -1) { + Log.w(TAG, "No event id"); + Toast.makeText(this, R.string.event_not_found, Toast.LENGTH_SHORT).show(); + finish(); + } + // If we do not support showing full screen event info in this configuration, // close the activity and show the event in AllInOne. Resources res = getResources(); diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java index 7c89075d..fffb631f 100644 --- a/src/com/android/calendar/EventInfoFragment.java +++ b/src/com/android/calendar/EventInfoFragment.java @@ -43,7 +43,6 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Rect; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -65,7 +64,6 @@ import android.text.format.Time; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; import android.text.style.URLSpan; import android.text.util.Linkify; import android.text.util.Rfc822Token; @@ -99,11 +97,12 @@ import com.android.calendar.CalendarController.EventInfo; import com.android.calendar.CalendarController.EventType; import com.android.calendar.CalendarEventModel.Attendee; import com.android.calendar.CalendarEventModel.ReminderEntry; +import com.android.calendar.alerts.QuickResponseActivity; import com.android.calendar.event.AttendeesView; import com.android.calendar.event.EditEventActivity; import com.android.calendar.event.EditEventHelper; import com.android.calendar.event.EventViewUtils; -import com.android.calendarcommon.EventRecurrence; +import com.android.calendarcommon2.EventRecurrence; import java.util.ArrayList; import java.util.Arrays; @@ -147,9 +146,11 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; private static final int TOKEN_QUERY_REMINDERS = 1 << 4; + private static final int TOKEN_QUERY_VISIBLE_CALENDARS = 1 << 5; private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT - | TOKEN_QUERY_REMINDERS; + | TOKEN_QUERY_REMINDERS | TOKEN_QUERY_VISIBLE_CALENDARS; + private int mCurrentQuery = 0; private static final String[] EVENT_PROJECTION = new String[] { @@ -163,8 +164,8 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper Events.DESCRIPTION, // 8 Events.EVENT_LOCATION, // 9 - Calendars.CALENDAR_ACCESS_LEVEL, // 10 - Events.DISPLAY_COLOR, // 11 + Calendars.CALENDAR_ACCESS_LEVEL, // 10 + Events.DISPLAY_COLOR, // 11 If SDK < 16, set to Calendars.CALENDAR_COLOR. Events.HAS_ATTENDEE_DATA, // 12 Events.ORGANIZER, // 13 Events.HAS_ALARM, // 14 @@ -193,7 +194,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private static final int EVENT_INDEX_CUSTOM_APP_PACKAGE = 17; private static final int EVENT_INDEX_CUSTOM_APP_URI = 18; - private static final String[] ATTENDEES_PROJECTION = new String[] { Attendees._ID, // 0 Attendees.ATTENDEE_NAME, // 1 @@ -211,6 +211,17 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private static final int ATTENDEES_INDEX_IDENTITY = 5; private static final int ATTENDEES_INDEX_ID_NAMESPACE = 6; + static { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[EVENT_INDEX_COLOR] = Calendars.CALENDAR_COLOR; + EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID; // dummy value + EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID; // dummy value + + ATTENDEES_PROJECTION[ATTENDEES_INDEX_IDENTITY] = Attendees._ID; // dummy value + ATTENDEES_PROJECTION[ATTENDEES_INDEX_ID_NAMESPACE] = Attendees._ID; // dummy value + } + } + private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " @@ -241,6 +252,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange static final String CALENDARS_WHERE = Calendars._ID + "=?"; static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; + static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?"; private static final String NANP_ALLOWED_SYMBOLS = "()+-*#."; private static final int NANP_MIN_DIGITS = 7; @@ -276,7 +288,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private boolean mCanModifyEvent; private boolean mIsBusyFreeCalendar; private int mNumOfAttendees; - private EditResponseHelper mEditResponseHelper; private boolean mDeleteDialogVisible = false; private DeleteEventHelper mDeleteHelper; @@ -296,6 +307,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange private TextView mWhere; private ExpandableTextView mDesc; private AttendeesView mLongAttendees; + private Button emailAttendeesButton; private Menu mMenu = null; private View mHeadlines; private ScrollView mScrollView; @@ -388,7 +400,9 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange // if the activity is finishing, then close the cursor and return final Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { - cursor.close(); + if (cursor != null) { + cursor.close(); + } return; } @@ -447,16 +461,25 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mRemindersCursor = Utils.matrixCursorFromCursor(cursor); initReminders(mView, mRemindersCursor); break; + case TOKEN_QUERY_VISIBLE_CALENDARS: + if (cursor.getCount() > 1) { + // Start duplicate calendars query to detect whether to add the calendar + // email to the calendar owner display. + String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); + mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, + Calendars.CONTENT_URI, CALENDARS_PROJECTION, + CALENDARS_DUPLICATE_NAME_WHERE, new String[] {displayName}, null); + } 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); + mCurrentQuery |= TOKEN_QUERY_DUPLICATE_CALENDARS; + } + break; case TOKEN_QUERY_DUPLICATE_CALENDARS: Resources res = activity.getResources(); SpannableStringBuilder sb = new SpannableStringBuilder(); - // Label - String label = res.getString(R.string.view_event_calendar_label); - sb.append(label).append(" "); - sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - // Calendar display name String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); sb.append(calendarName); @@ -464,15 +487,19 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange // 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)) { + 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); break; } cursor.close(); sendAccessibilityEventIfQueryDone(token); - // All queries are done, show the view + + // All queries are done, show the view. if (mCurrentQuery == TOKEN_QUERY_ALL) { if (mLoadingMsgView.getAlpha() == 1) { // Loading message is showing, let it stay a bit more (to prevent @@ -741,7 +768,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } // Create a listener for the email guests button - View emailAttendeesButton = mView.findViewById(R.id.email_attendees_button); + emailAttendeesButton = (Button) mView.findViewById(R.id.email_attendees_button); if (emailAttendeesButton != null) { emailAttendeesButton.setOnClickListener(new View.OnClickListener() { @Override @@ -850,9 +877,13 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID); mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); } else { - String identity = mAttendeesCursor.getString(ATTENDEES_INDEX_IDENTITY); - String idNamespace = mAttendeesCursor.getString( - ATTENDEES_INDEX_ID_NAMESPACE); + String identity = null; + String idNamespace = null; + + if (Utils.isJellybeanOrLater()) { + identity = mAttendeesCursor.getString(ATTENDEES_INDEX_IDENTITY); + idNamespace = mAttendeesCursor.getString(ATTENDEES_INDEX_ID_NAMESPACE); + } // Don't show your own status in the list because: // 1) it doesn't make sense for event without other guests. @@ -926,32 +957,28 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange // Delete button - start a delete query that calls a runnable that close // the info activity - switch (item.getItemId()) { - case android.R.id.home: - Utils.returnToCalendarHome(mContext); - mActivity.finish(); - return true; - case R.id.info_action_edit: - Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); - Intent intent = new Intent(Intent.ACTION_EDIT, uri); - intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis); - intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis); - intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay); - intent.setClass(mActivity, EditEventActivity.class); - intent.putExtra(EVENT_EDIT_ON_LAUNCH, true); - startActivity(intent); - mActivity.finish(); - break; - case R.id.info_action_delete: - mDeleteHelper = - new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */); - mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this); - mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener()); - mDeleteDialogVisible = true; - mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); - break; - default: - break; + final int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + Utils.returnToCalendarHome(mContext); + mActivity.finish(); + return true; + } else if (itemId == R.id.info_action_edit) { + Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); + Intent intent = new Intent(Intent.ACTION_EDIT, uri); + intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis); + intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis); + intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay); + intent.setClass(mActivity, EditEventActivity.class); + intent.putExtra(EVENT_EDIT_ON_LAUNCH, true); + startActivity(intent); + mActivity.finish(); + } else if (itemId == R.id.info_action_delete) { + mDeleteHelper = + new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */); + mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this); + mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener()); + mDeleteDialogVisible = true; + mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); } return super.onOptionsItemSelected(item); } @@ -1075,18 +1102,14 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange public static int getResponseFromButtonId(int buttonId) { int response; - switch (buttonId) { - case R.id.response_yes: - response = Attendees.ATTENDEE_STATUS_ACCEPTED; - break; - case R.id.response_maybe: - response = Attendees.ATTENDEE_STATUS_TENTATIVE; - break; - case R.id.response_no: - response = Attendees.ATTENDEE_STATUS_DECLINED; - break; - default: - response = Attendees.ATTENDEE_STATUS_NONE; + if (buttonId == R.id.response_yes) { + response = Attendees.ATTENDEE_STATUS_ACCEPTED; + } else if (buttonId == R.id.response_maybe) { + response = Attendees.ATTENDEE_STATUS_TENTATIVE; + } else if (buttonId == R.id.response_no) { + response = Attendees.ATTENDEE_STATUS_DECLINED; + } else { + response = Attendees.ATTENDEE_STATUS_NONE; } return response; } @@ -1233,7 +1256,9 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } // Launch Custom App - updateCustomAppButton(); + if (Utils.isJellybeanOrLater()) { + updateCustomAppButton(); + } } private void updateCustomAppButton() { @@ -1372,6 +1397,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange * 94043 # too short, ignore * 123456789012 # too long, ignore * +1 (650) 555-1212 # 11 digits, spaces + * (650) 555 5555 # Second space, only when first is present. * (650) 555-1212, (650) 555-1213 # two numbers, return first * 1-650-555-1212 # 11 digits with leading '1' * *#650.555.1212#*! # 10 digits, include #*, ignore trailing '!' @@ -1381,10 +1407,17 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange * between the initial '1' and/or after the area code. */ + // Check for "tel:" URI prefix. + if (text.length() > startPos+4 + && text.subSequence(startPos, startPos+4).toString().equalsIgnoreCase("tel:")) { + startPos += 4; + } + int endPos = text.length(); int curPos = startPos; int foundDigits = 0; char firstDigit = 'x'; + boolean foundWhiteSpaceAfterAreaCode = false; while (curPos <= endPos) { char ch; @@ -1404,8 +1437,13 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange return -1; } } else if (Character.isWhitespace(ch)) { - if (!( (firstDigit == '1' && (foundDigits == 1 || foundDigits == 4)) || - (foundDigits == 3)) ) { + if ( (firstDigit == '1' && foundDigits == 4) || + (foundDigits == 3)) { + foundWhiteSpaceAfterAreaCode = true; + } else if (firstDigit == '1' && foundDigits == 1) { + } else if (foundWhiteSpaceAfterAreaCode + && ( (firstDigit == '1' && (foundDigits == 7)) || (foundDigits == 6))) { + } else { break; } } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) { @@ -1425,6 +1463,24 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange return -1; } + private static int indexFirstNonWhitespaceChar(CharSequence str) { + for (int i = 0; i < str.length(); i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return i; + } + } + return -1; + } + + private static int indexLastNonWhitespaceChar(CharSequence str) { + for (int i = str.length() - 1; i >= 0; i--) { + if (!Character.isWhitespace(str.charAt(i))) { + return i; + } + } + return -1; + } + /** * Replaces stretches of text that look like addresses and phone numbers with clickable * links. @@ -1435,12 +1491,38 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange /* * If the text includes a street address like "1600 Amphitheater Parkway, 94043", * the current Linkify code will identify "94043" as a phone number and invite - * you to dial it (and not provide a map link for the address). We want to - * have better recognition of phone numbers without losing any of the existing - * annotations. - * - * Ideally this would be addressed by improving Linkify. For now we manage it as - * a second pass over the text. + * you to dial it (and not provide a map link for the address). For outside US, + * use Linkify result iff it spans the entire text. Otherwise send the user to maps. + */ + String defaultPhoneRegion = System.getProperty("user.region", "US"); + if (!defaultPhoneRegion.equals("US")) { + CharSequence origText = textView.getText(); + Linkify.addLinks(textView, Linkify.ALL); + + // If Linkify links the entire text, use that result. + if (textView.getText() instanceof Spannable) { + Spannable spanText = (Spannable) textView.getText(); + URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class); + if (spans.length == 1) { + int linkStart = spanText.getSpanStart(spans[0]); + int linkEnd = spanText.getSpanEnd(spans[0]); + if (linkStart <= indexFirstNonWhitespaceChar(origText) && + linkEnd >= indexLastNonWhitespaceChar(origText) + 1) { + return; + } + } + } + + // Otherwise default to geo. + textView.setText(origText); + Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); + return; + } + + /* + * For within US, we want to have better recognition of phone numbers without losing + * any of the existing annotations. Ideally this would be addressed by improving Linkify. + * For now we manage it as a second pass over the text. * * URIs and e-mail addresses are pretty easy to pick out of text. Phone numbers * are a bit tricky because they have radically different formats in different @@ -1450,18 +1532,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange * pretty narrowly defined, so it won't often match. * * The RFC 3966 specification defines the format of a "tel:" URI. - */ - - /* - * If we're in the US, handle this specially. Otherwise, punt to Linkify. - */ - String defaultPhoneRegion = System.getProperty("user.region", "US"); - if (!defaultPhoneRegion.equals("US")) { - Linkify.addLinks(textView, Linkify.ALL); - return; - } - - /* + * * Start by letting Linkify find anything that isn't a phone number. We have to let it * run first because every invocation removes all previous URLSpan annotations. * @@ -1643,10 +1714,9 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); - // start duplicate calendars query - mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI, - CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE, - new String[] {displayName}, null); + // start visible calendars query + mHandler.startQuery(TOKEN_QUERY_VISIBLE_CALENDARS, null, Calendars.CONTENT_URI, + CALENDARS_PROJECTION, CALENDARS_VISIBLE_WHERE, new String[] {"1"}, null); mEventOrganizerEmail = mEventCursor.getString(EVENT_INDEX_ORGANIZER); mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(mEventOrganizerEmail); @@ -1748,48 +1818,51 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange mLongAttendees.setVisibility(View.GONE); } - updateEmailAttendees(); + if (hasEmailableAttendees()) { + setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE); + if (emailAttendeesButton != null) { + emailAttendeesButton.setText(R.string.email_guests_label); + } + } else if (hasEmailableOrganizer()) { + setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE); + if (emailAttendeesButton != null) { + emailAttendeesButton.setText(R.string.email_organizer_label); + } + } else { + setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE); + } } /** - * Initializes the list of 'to' and 'cc' emails from the attendee list. + * Returns true if there is at least 1 attendee that is not the viewer. */ - private void updateEmailAttendees() { - // The declined attendees will go in the 'cc' line, all others will go in the 'to' line. - mToEmails = new ArrayList<String>(); + private boolean hasEmailableAttendees() { for (Attendee attendee : mAcceptedAttendees) { - addIfEmailable(mToEmails, attendee.mEmail); + if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) { + return true; + } } for (Attendee attendee : mTentativeAttendees) { - addIfEmailable(mToEmails, attendee.mEmail); + if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) { + return true; + } } for (Attendee attendee : mNoResponseAttendees) { - addIfEmailable(mToEmails, attendee.mEmail); - } - mCcEmails = new ArrayList<String>(); - for (Attendee attendee : this.mDeclinedAttendees) { - addIfEmailable(mCcEmails, attendee.mEmail); - } - - // The meeting organizer doesn't appear as an attendee sometimes (particularly - // when viewing someone else's calendar), so add the organizer now. - if (mEventOrganizerEmail != null && !mToEmails.contains(mEventOrganizerEmail) && - !mCcEmails.contains(mEventOrganizerEmail)) { - addIfEmailable(mToEmails, mEventOrganizerEmail); + if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) { + return true; + } } - - // The Email app behaves strangely when there is nothing in the 'mailto' part, - // so move all the 'cc' emails to the 'to' list. Gmail works fine though. - if (mToEmails.size() <= 0 && mCcEmails.size() > 0) { - mToEmails.addAll(mCcEmails); - mCcEmails.clear(); + for (Attendee attendee : mDeclinedAttendees) { + if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) { + return true; + } } + return false; + } - if (mToEmails.size() <= 0) { - setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE); - } else { - setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE); - } + private boolean hasEmailableOrganizer() { + return mEventOrganizerEmail != null && + Utils.isEmailableFrom(mEventOrganizerEmail, mSyncAccountName); } public void initReminders(View view, Cursor cursor) { @@ -2094,27 +2167,14 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange } /** - * Adds the attendee's email to the list if: - * (1) the attendee is not a resource like a conference room or another calendar. - * Catch most of these by filtering out suffix calendar.google.com. - * (2) the attendee is not the viewer, to prevent mailing himself. - */ - private void addIfEmailable(ArrayList<String> emailList, String email) { - if (Utils.isEmailableFrom(email, mSyncAccountName)) { - emailList.add(email); - } - } - - /** * Email all the attendees of the event, except for the viewer (so as to not email * himself) and resources like conference rooms. */ private void emailAttendees() { - String eventTitle = (mTitle == null || mTitle.getText() == null) ? null : - mTitle.getText().toString(); - Intent emailIntent = Utils.createEmailAttendeesIntent(getActivity().getResources(), - eventTitle, null /* body */, mToEmails, mCcEmails, mCalendarOwnerAccount); - startActivity(emailIntent); + Intent i = new Intent(getActivity(), QuickResponseActivity.class); + i.putExtra(QuickResponseActivity.EXTRA_EVENT_ID, mEventId); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(i); } /** diff --git a/src/com/android/calendar/EventRecurrenceFormatter.java b/src/com/android/calendar/EventRecurrenceFormatter.java index db2a3770..5325ba6e 100644 --- a/src/com/android/calendar/EventRecurrenceFormatter.java +++ b/src/com/android/calendar/EventRecurrenceFormatter.java @@ -16,7 +16,7 @@ package com.android.calendar; -import com.android.calendarcommon.EventRecurrence; +import com.android.calendarcommon2.EventRecurrence; import android.content.res.Resources; import android.text.format.DateUtils; diff --git a/src/com/android/calendar/GeneralPreferences.java b/src/com/android/calendar/GeneralPreferences.java index ea79d574..abaa2b66 100644 --- a/src/com/android/calendar/GeneralPreferences.java +++ b/src/com/android/calendar/GeneralPreferences.java @@ -71,6 +71,7 @@ public class GeneralPreferences extends PreferenceFragment implements public static final int REMINDER_DEFAULT_TIME = 10; // in minutes public static final String KEY_DEFAULT_CELL_HEIGHT = "preferences_default_cell_height"; + public static final String KEY_VERSION = "preferences_version"; /** Key to SharePreference for default view (CalendarController.ViewType) */ public static final String KEY_START_VIEW = "preferred_startView"; diff --git a/src/com/android/calendar/GoogleCalendarUriIntentFilter.java b/src/com/android/calendar/GoogleCalendarUriIntentFilter.java index 77e337b1..4a3c0cbd 100644 --- a/src/com/android/calendar/GoogleCalendarUriIntentFilter.java +++ b/src/com/android/calendar/GoogleCalendarUriIntentFilter.java @@ -38,8 +38,8 @@ import android.text.TextUtils; import android.util.Base64; import android.util.Log; -import com.android.calendarcommon.DateException; -import com.android.calendarcommon.Duration; +import com.android.calendarcommon2.DateException; +import com.android.calendarcommon2.Duration; public class GoogleCalendarUriIntentFilter extends Activity { private static final String TAG = "GoogleCalendarUriIntentFilter"; diff --git a/src/com/android/calendar/SearchActivity.java b/src/com/android/calendar/SearchActivity.java index f7a26b3a..4775b8d7 100644 --- a/src/com/android/calendar/SearchActivity.java +++ b/src/com/android/calendar/SearchActivity.java @@ -250,8 +250,14 @@ public class SearchActivity extends Activity implements CalendarController.Event // replace the default top layer drawable of the today icon with a custom drawable // that shows the day of the month of today - LayerDrawable icon = (LayerDrawable)menu.findItem(R.id.action_today).getIcon(); - Utils.setTodayIcon(icon, this, Utils.getTimeZone(SearchActivity.this, mTimeChangesUpdater)); + MenuItem menuItem = menu.findItem(R.id.action_today); + if (Utils.isJellybeanOrLater()) { + LayerDrawable icon = (LayerDrawable) menuItem.getIcon(); + Utils.setTodayIcon( + icon, this, Utils.getTimeZone(SearchActivity.this, mTimeChangesUpdater)); + } else { + menuItem.setIcon(R.drawable.ic_menu_today_no_date_holo_light); + } MenuItem item = menu.findItem(R.id.action_search); item.expandActionView(); @@ -267,22 +273,22 @@ public class SearchActivity extends Activity implements CalendarController.Event @Override public boolean onOptionsItemSelected(MenuItem item) { Time t = null; - switch (item.getItemId()) { - case R.id.action_today: - t = new Time(); - t.setToNow(); - mController.sendEvent(this, EventType.GO_TO, t, null, -1, ViewType.CURRENT); - return true; - case R.id.action_search: - return false; - case R.id.action_settings: - mController.sendEvent(this, EventType.LAUNCH_SETTINGS, null, null, 0, 0); - return true; - case android.R.id.home: - Utils.returnToCalendarHome(this); - return true; - default: - return false; + final int itemId = item.getItemId(); + if (itemId == R.id.action_today) { + t = new Time(); + t.setToNow(); + mController.sendEvent(this, EventType.GO_TO, t, null, -1, ViewType.CURRENT); + return true; + } else if (itemId == R.id.action_search) { + return false; + } else if (itemId == R.id.action_settings) { + mController.sendEvent(this, EventType.LAUNCH_SETTINGS, null, null, 0, 0); + return true; + } else if (itemId == android.R.id.home) { + Utils.returnToCalendarHome(this); + return true; + } else { + return false; } } diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java index 996f9b13..3afcfd44 100644 --- a/src/com/android/calendar/Utils.java +++ b/src/com/android/calendar/Utils.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.database.MatrixCursor; @@ -32,6 +33,7 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; @@ -121,6 +123,14 @@ public class Utils { private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME); private static boolean mAllowWeekForDetailView = false; private static long mTardis = 0; + private static String sVersion = null; + + /** + * Returns whether the SDK is the Jellybean release or later. + */ + public static boolean isJellybeanOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } public static int getViewTypeFromIntentAndSharedPref(Activity activity) { Intent intent = activity.getIntent(); @@ -254,8 +264,8 @@ public class Utils { public static void setSharedPreference(Context context, String key, String[] values) { SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); LinkedHashSet<String> set = new LinkedHashSet<String>(); - for (int i = 0; i < values.length; i++) { - set.add(values[i]); + for (String value : values) { + set.add(value); } prefs.edit().putStringSet(key, set).apply(); } @@ -311,6 +321,10 @@ public class Utils { } public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { + if (cursor == null) { + return null; + } + String[] columnNames = cursor.getColumnNames(); if (columnNames == null) { columnNames = new String[] {}; @@ -628,7 +642,9 @@ public class Utils { } public static int getDisplayColorFromColor(int color) { - // STOPSHIP - Finalize color adjustment algorithm before shipping + if (!isJellybeanOrLater()) { + return color; + } float[] hsv = new float[3]; Color.colorToHSV(color, hsv); @@ -1389,6 +1405,13 @@ public class Utils { // are multiple email accounts. Intent emailIntent = new Intent(android.content.Intent.ACTION_SENDTO, Uri.parse(uri)); emailIntent.putExtra("fromAccountString", ownerAccount); + + // Workaround a Email bug that overwrites the body with this intent extra. If not + // set, it clears the body. + if (body != null) { + emailIntent.putExtra(Intent.EXTRA_TEXT, body); + } + return Intent.createChooser(emailIntent, resources.getString(R.string.email_picker_label)); } @@ -1487,4 +1510,20 @@ public class Utils { return s; } + + /** + * Return the app version code. + */ + public static String getVersionCode(Context context) { + if (sVersion == null) { + try { + sVersion = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + // Can't find version; just leave it blank. + Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName); + } + } + return sVersion; + } } diff --git a/src/com/android/calendar/agenda/AgendaByDayAdapter.java b/src/com/android/calendar/agenda/AgendaByDayAdapter.java index 9e4c27b7..8fa44820 100644 --- a/src/com/android/calendar/agenda/AgendaByDayAdapter.java +++ b/src/com/android/calendar/agenda/AgendaByDayAdapter.java @@ -88,6 +88,14 @@ public class AgendaByDayAdapter extends BaseAdapter { return mRowInfo.get(position).mInstanceId; } + public long getStartTime(int position) { + if (mRowInfo == null || position >= mRowInfo.size()) { + return -1; + } + return mRowInfo.get(position).mEventStartTimeMilli; + } + + // Returns the position of a header of a specific item public int getHeaderPosition(int position) { if (mRowInfo == null || position >= mRowInfo.size()) { @@ -362,10 +370,6 @@ public class AgendaByDayAdapter extends BaseAdapter { prevStartDay = startDay; } - // Add in the event for this cursor position - rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime, - instanceId, allDay)); - // If this event spans multiple days, then add it to the multipleDay // list. int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY); @@ -373,9 +377,17 @@ public class AgendaByDayAdapter extends BaseAdapter { // Skip over the days outside of the adapter's range endDay = Math.min(endDay, dayAdapterInfo.end); if (endDay > startDay) { - multipleDayList.add(new MultipleDayInfo(position, endDay, id, - Utils.getNextMidnight(tempTime, startTime, mTimeZone), + long nextMidnight = Utils.getNextMidnight(tempTime, startTime, mTimeZone); + multipleDayList.add(new MultipleDayInfo(position, endDay, id, nextMidnight, endTime, instanceId, allDay)); + // Add in the event for this cursor position - since it is the start of a multi-day + // event, the end time is midnight + rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, + nextMidnight, instanceId, allDay)); + } else { + // Add in the event for this cursor position + rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime, + instanceId, allDay)); } } @@ -496,7 +508,7 @@ public class AgendaByDayAdapter extends BaseAdapter { } long millis = time.toMillis(false /* use isDst */); long minDistance = Integer.MAX_VALUE; // some big number - long IdFoundMinDistance = Integer.MAX_VALUE; // some big number + long idFoundMinDistance = Integer.MAX_VALUE; // some big number int minIndex = 0; int idFoundMinIndex = 0; int eventInTimeIndex = -1; @@ -528,8 +540,8 @@ public class AgendaByDayAdapter extends BaseAdapter { // Not an exact match, Save event index if it is the closest to time so far long distance = Math.abs(millis - row.mEventStartTimeMilli); - if (distance < minDistance) { - IdFoundMinDistance = distance; + if (distance < idFoundMinDistance) { + idFoundMinDistance = distance; idFoundMinIndex = index; } idFound = true; diff --git a/src/com/android/calendar/agenda/AgendaFragment.java b/src/com/android/calendar/agenda/AgendaFragment.java index d40a5208..ff1659ab 100644 --- a/src/com/android/calendar/agenda/AgendaFragment.java +++ b/src/com/android/calendar/agenda/AgendaFragment.java @@ -68,6 +68,9 @@ public class AgendaFragment extends Fragment implements CalendarController.Event private boolean mOnAttachAllDay = false; private AgendaWindowAdapter mAdapter = null; private boolean mForceReplace = true; + private long mLastShownEventId = -1; + + // Tracks the time of the top visible view in order to send UPDATE_TITLE messages to the action // bar. @@ -91,11 +94,14 @@ public class AgendaFragment extends Fragment implements CalendarController.Event public AgendaFragment(long timeMillis, boolean usedForSearch) { mInitialTimeMillis = timeMillis; mTime = new Time(); + mLastHandledEventTime = new Time(); + if (mInitialTimeMillis == 0) { mTime.setToNow(); } else { mTime.set(mInitialTimeMillis); } + mLastHandledEventTime.set(mTime); mUsedForSearch = usedForSearch; } @@ -238,15 +244,35 @@ public class AgendaFragment extends Fragment implements CalendarController.Event if (mAgendaListView == null) { return; } - long firstVisibleTime = mAgendaListView.getFirstVisibleTime(); - if (firstVisibleTime > 0) { - mTime.set(firstVisibleTime); - mController.setTime(firstVisibleTime); - outState.putLong(BUNDLE_KEY_RESTORE_TIME, firstVisibleTime); - if (DEBUG) { - Log.v(TAG, "onSaveInstanceState " + mTime.toString()); + if (mShowEventDetailsWithAgenda) { + long timeToSave; + if (mLastHandledEventTime != null) { + timeToSave = mLastHandledEventTime.toMillis(true); + mTime.set(mLastHandledEventTime); + } else { + timeToSave = System.currentTimeMillis(); + mTime.set(timeToSave); + } + outState.putLong(BUNDLE_KEY_RESTORE_TIME, timeToSave); + mController.setTime(timeToSave); + } else { + AgendaWindowAdapter.EventInfo e = mAgendaListView.getFirstVisibleEvent(); + if (e != null) { + long firstVisibleTime = mAgendaListView.getFirstVisibleTime(e); + if (firstVisibleTime > 0) { + mTime.set(firstVisibleTime); + mController.setTime(firstVisibleTime); + outState.putLong(BUNDLE_KEY_RESTORE_TIME, firstVisibleTime); + } + // Tell AllInOne the event id of the first visible event in the list. The id will be + // used in the GOTO when AllInOne is restored so that Agenda Fragment can select a + // specific event and not just the time. + mLastShownEventId = e.id; } } + if (DEBUG) { + Log.v(TAG, "onSaveInstanceState " + mTime.toString()); + } long selectedInstance = mAgendaListView.getSelectedInstanceId(); if (selectedInstance >= 0) { @@ -288,15 +314,16 @@ public class AgendaFragment extends Fragment implements CalendarController.Event } private void goTo(EventInfo event, boolean animate) { + if (event.selectedTime != null) { + mTime.set(event.selectedTime); + } else if (event.startTime != null) { + mTime.set(event.startTime); + } if (mAgendaListView == null) { // The view hasn't been set yet. Just save the time and use it // later. - mTime.set(event.startTime); return; } - if (event.startTime != null) { - mTime.set(event.startTime); - } mAgendaListView.goTo(mTime, event.id, mQuery, false, ((event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0 && mShowEventDetailsWithAgenda) ? true : false); @@ -339,7 +366,8 @@ public class AgendaFragment extends Fragment implements CalendarController.Event // TODO support event_id // TODO figure out the animate bit mLastHandledEventId = event.id; - mLastHandledEventTime = event.startTime; + mLastHandledEventTime = + (event.selectedTime != null) ? event.selectedTime : event.startTime; goTo(event, true); } else if (event.eventType == EventType.SEARCH) { search(event.query, event.startTime); @@ -348,6 +376,9 @@ public class AgendaFragment extends Fragment implements CalendarController.Event } } + public long getLastShowEventId() { + return mLastShownEventId; + } // Shows the selected event in the Agenda view private void showEventInfo(EventInfo event, boolean allDay, boolean replaceFragment) { @@ -358,6 +389,8 @@ public class AgendaFragment extends Fragment implements CalendarController.Event return; } + mLastShownEventId = event.id; + // Create a fragment to show the event to the side of the agenda list if (mShowEventDetailsWithAgenda) { FragmentManager fragmentManager = getFragmentManager(); @@ -393,17 +426,6 @@ public class AgendaFragment extends Fragment implements CalendarController.Event fOld.reloadEvents(); } } -// else { -// Intent intent = new Intent(Intent.ACTION_VIEW); -// Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, event.id); -// intent.setData(eventUri); -// intent.setClass(getActivity(), AllInOneActivity.class); -// intent.putExtra(EVENT_BEGIN_TIME, event.startTime != null ? event.startTime -// .toMillis(true) : -1); -// intent.putExtra(EVENT_END_TIME, event.endTime != null ? event.endTime.toMillis(true) -// : -1); -// startActivity(intent); -// } } // OnScrollListener implementation to update the date on the pull-down menu of the app diff --git a/src/com/android/calendar/agenda/AgendaListView.java b/src/com/android/calendar/agenda/AgendaListView.java index 25a66608..f0a0c870 100644 --- a/src/com/android/calendar/agenda/AgendaListView.java +++ b/src/com/android/calendar/agenda/AgendaListView.java @@ -177,13 +177,21 @@ public class AgendaListView extends ListView implements OnItemClickListener { mWindowAdapter.setSelectedView(v); // If events are shown to the side of the agenda list , do nothing - // when the same - // event is selected , otherwise show the selected event. + // when the same event is selected , otherwise show the selected event. if (event != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() || !mShowEventDetailsWithAgenda)) { long startTime = event.begin; long endTime = event.end; + // Holder in view holds the start of the specific part of a multi-day event , + // use it for the goto + long holderStartTime; + Object holder = v.getTag(); + if (holder instanceof AgendaAdapter.ViewHolder) { + holderStartTime = ((AgendaAdapter.ViewHolder) holder).startTimeMilli; + } else { + holderStartTime = startTime; + } if (event.allDay) { startTime = Utils.convertAlldayLocalToUTC(mTime, startTime, mTimeZone); endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone); @@ -192,8 +200,7 @@ public class AgendaListView extends ListView implements OnItemClickListener { CalendarController controller = CalendarController.getInstance(mContext); controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, event.id, startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong( - Attendees.ATTENDEE_STATUS_NONE, event.allDay), - controller.getTime()); + Attendees.ATTENDEE_STATUS_NONE, event.allDay), holderStartTime); } } } @@ -202,7 +209,7 @@ public class AgendaListView extends ListView implements OnItemClickListener { boolean refreshEventInfo) { if (time == null) { time = mTime; - long goToTime = getFirstVisibleTime(); + long goToTime = getFirstVisibleTime(null); if (goToTime <= 0) { goToTime = System.currentTimeMillis(); } @@ -250,14 +257,39 @@ public class AgendaListView extends ListView implements OnItemClickListener { return event.begin; } } - return getFirstVisibleTime(); + return getFirstVisibleTime(null); } public AgendaAdapter.ViewHolder getSelectedViewHolder() { return mWindowAdapter.getSelectedViewHolder(); } - public long getFirstVisibleTime() { + public long getFirstVisibleTime(EventInfo e) { + EventInfo event = e; + if (e == null) { + event = getFirstVisibleEvent(); + } + if (event != null) { + Time t = new Time(mTimeZone); + t.set(event.begin); + // Save and restore the time since setJulianDay sets the time to 00:00:00 + int hour = t.hour; + int minute = t.minute; + int second = t.second; + t.setJulianDay(event.startDay); + t.hour = hour; + t.minute = minute; + t.second = second; + if (DEBUG) { + t.normalize(true); + Log.d(TAG, "first position had time " + t.toString()); + } + return t.normalize(false); + } + return 0; + } + + public EventInfo getFirstVisibleEvent() { int position = getFirstVisiblePosition(); if (DEBUG) { Log.v(TAG, "getFirstVisiblePosition = " + position); @@ -277,26 +309,9 @@ public class AgendaListView extends ListView implements OnItemClickListener { } } - EventInfo event = mWindowAdapter.getEventByPosition(position, + return mWindowAdapter.getEventByPosition(position, false /* startDay = date separator date instead of actual event startday */); - if (event != null) { - Time t = new Time(mTimeZone); - t.set(event.begin); - // Save and restore the time since setJulianDay sets the time to 00:00:00 - int hour = t.hour; - int minute = t.minute; - int second = t.second; - t.setJulianDay(event.startDay); - t.hour = hour; - t.minute = minute; - t.second = second; - if (DEBUG) { - t.normalize(true); - Log.d(TAG, "position " + position + " had time " + t.toString()); - } - return t.normalize(false); - } - return 0; + } public int getJulianDayFromPosition(int position) { diff --git a/src/com/android/calendar/agenda/AgendaWindowAdapter.java b/src/com/android/calendar/agenda/AgendaWindowAdapter.java index 8ac14eb3..80d8a9d3 100644 --- a/src/com/android/calendar/agenda/AgendaWindowAdapter.java +++ b/src/com/android/calendar/agenda/AgendaWindowAdapter.java @@ -107,7 +107,7 @@ public class AgendaWindowAdapter extends BaseAdapter Instances.EVENT_LOCATION, // 2 Instances.ALL_DAY, // 3 Instances.HAS_ALARM, // 4 - Instances.DISPLAY_COLOR, // 5 + Instances.DISPLAY_COLOR, // 5 If SDK < 16, set to Instances.CALENDAR_COLOR. Instances.RRULE, // 6 Instances.BEGIN, // 7 Instances.END, // 8 @@ -121,6 +121,12 @@ public class AgendaWindowAdapter extends BaseAdapter Instances.EVENT_TIMEZONE, // 16 }; + static { + if (!Utils.isJellybeanOrLater()) { + PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR; + } + } + // Listview may have a bug where the index/position is not consistent when there's a header. // position == positionInListView - OFF_BY_ONE_BUG // TODO Need to look into this. @@ -197,6 +203,14 @@ public class AgendaWindowAdapter extends BaseAdapter } }; + private final Handler mDataChangedHandler = new Handler(); + private final Runnable mDataChangedRunnable = new Runnable() { + @Override + public void run() { + notifyDataSetChanged(); + } + }; + private boolean mShuttingDown; private boolean mHideDeclined; @@ -667,12 +681,7 @@ public class AgendaWindowAdapter extends BaseAdapter long newInstanceId = findInstanceIdFromPosition(gotoPosition); if (newInstanceId != getSelectedInstanceId()) { setSelectedInstanceId(newInstanceId); - new Handler().post(new Runnable() { - @Override - public void run() { - notifyDataSetChanged(); - } - }); + mDataChangedHandler.post(mDataChangedRunnable); Cursor tempCursor = getCursorByPosition(gotoPosition); if (tempCursor != null) { int tempCursorPosition = getCursorPositionByPosition(gotoPosition); @@ -684,7 +693,7 @@ public class AgendaWindowAdapter extends BaseAdapter event.id, event.begin, event.end, 0, 0, CalendarController.EventInfo.buildViewExtraLong( Attendees.ATTENDEE_STATUS_NONE, - event.allDay), -1); + event.allDay), goToTime.toMillis(false)); } } } @@ -1028,10 +1037,12 @@ public class AgendaWindowAdapter extends BaseAdapter if (tempCursor != null) { EventInfo event = buildEventInfoFromCursor(tempCursor, tempCursorPosition, false); + long selectedTime = findStartTimeFromPosition(newPosition); CalendarController.getInstance(mContext).sendEventRelatedEventWithExtra( this, EventType.VIEW_EVENT, event.id, event.begin, event.end, 0, 0, CalendarController.EventInfo.buildViewExtraLong( - Attendees.ATTENDEE_STATUS_NONE, event.allDay), -1); + Attendees.ATTENDEE_STATUS_NONE, event.allDay), + selectedTime); } } } else { @@ -1288,6 +1299,15 @@ public class AgendaWindowAdapter extends BaseAdapter return -1; } + private long findStartTimeFromPosition(int position) { + DayAdapterInfo info = getAdapterInfoByPosition(position); + if (info != null) { + return info.dayAdapter.getStartTime(position - info.offset); + } + return -1; + } + + private Cursor getCursorByPosition(int position) { DayAdapterInfo info = getAdapterInfoByPosition(position); if (info != null) { diff --git a/src/com/android/calendar/alerts/AlarmManagerInterface.java b/src/com/android/calendar/alerts/AlarmManagerInterface.java new file mode 100644 index 00000000..5ee83734 --- /dev/null +++ b/src/com/android/calendar/alerts/AlarmManagerInterface.java @@ -0,0 +1,10 @@ +package com.android.calendar.alerts; + +import android.app.PendingIntent; + +/** + * AlarmManager abstracted to an interface for testability. + */ +public interface AlarmManagerInterface { + public void set(int type, long triggerAtMillis, PendingIntent operation); +} diff --git a/src/com/android/calendar/alerts/AlertActivity.java b/src/com/android/calendar/alerts/AlertActivity.java index 668c7f13..12d7b10a 100644 --- a/src/com/android/calendar/alerts/AlertActivity.java +++ b/src/com/android/calendar/alerts/AlertActivity.java @@ -124,27 +124,11 @@ public class AlertActivity extends Activity implements OnClickListener { } @Override - protected void onInsertComplete(int token, Object cookie, Uri uri) { - if (uri != null) { - Long alarmTime = (Long) cookie; - - if (alarmTime != 0) { - // Set a new alarm to go off after the snooze delay. - // TODO make provider schedule this automatically when - // inserting an alarm - AlertUtils.scheduleAlarm(AlertActivity.this, null, alarmTime); - } - } - } - - @Override protected void onUpdateComplete(int token, Object cookie, int result) { // Ignore } } - - private final OnItemClickListener mViewListener = new OnItemClickListener() { @Override @@ -164,9 +148,12 @@ public class AlertActivity extends Activity implements OnClickListener { Intent eventIntent = AlertUtils.buildEventViewIntent(AlertActivity.this, id, startMillis, endMillis); - TaskStackBuilder.create(AlertActivity.this) - .addParentStack(EventInfoActivity.class).addNextIntent(eventIntent) - .startActivities(); + if (Utils.isJellybeanOrLater()) { + TaskStackBuilder.create(AlertActivity.this).addParentStack(EventInfoActivity.class) + .addNextIntent(eventIntent).startActivities(); + } else { + alertActivity.startActivity(eventIntent); + } alertActivity.finish(); } @@ -212,6 +199,12 @@ public class AlertActivity extends Activity implements OnClickListener { } } + void closeActivityIfEmpty() { + if (mCursor != null && !mCursor.isClosed() && mCursor.getCount() == 0) { + AlertActivity.this.finish(); + } + } + @Override protected void onStop() { super.onStop(); diff --git a/src/com/android/calendar/alerts/AlertAdapter.java b/src/com/android/calendar/alerts/AlertAdapter.java index fc13088c..76522834 100644 --- a/src/com/android/calendar/alerts/AlertAdapter.java +++ b/src/com/android/calendar/alerts/AlertAdapter.java @@ -35,13 +35,15 @@ import java.util.TimeZone; public class AlertAdapter extends ResourceCursorAdapter { + private static AlertActivity alertActivity; private static boolean mFirstTime = true; private static int mTitleColor; private static int mOtherColor; // non-title fields private static int mPastEventColor; - public AlertAdapter(Context context, int resource) { - super(context, resource, null); + public AlertAdapter(AlertActivity activity, int resource) { + super(activity, resource, null); + this.alertActivity = activity; } @Override @@ -143,4 +145,12 @@ public class AlertAdapter extends ResourceCursorAdapter { whereView.setVisibility(View.VISIBLE); } } + + @Override + protected void onContentChanged () { + super.onContentChanged(); + + // Prevent empty popup notification. + alertActivity.closeActivityIfEmpty(); + } } diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java index e7a63bd3..a0a82d54 100644 --- a/src/com/android/calendar/alerts/AlertReceiver.java +++ b/src/com/android/calendar/alerts/AlertReceiver.java @@ -37,6 +37,8 @@ import android.text.TextUtils; import android.text.style.RelativeSizeSpan; import android.text.style.TextAppearanceSpan; import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; import com.android.calendar.R; import com.android.calendar.Utils; @@ -217,17 +219,17 @@ public class AlertReceiver extends BroadcastReceiver { public static NotificationWrapper makeBasicNotification(Context context, String title, String summaryText, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup) { - - Notification n = makeBasicNotificationBuilder(context, title, summaryText, startMillis, - endMillis, eventId, notificationId, doPopup, false, false).build(); - + int notificationId, boolean doPopup, int priority) { + Notification n = buildBasicNotification(new Notification.Builder(context), + context, title, summaryText, startMillis, endMillis, eventId, notificationId, + doPopup, priority, false); return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup); } - private static Notification.Builder makeBasicNotificationBuilder(Context context, String title, - String summaryText, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup, boolean highPriority, boolean addActionButtons) { + private static Notification buildBasicNotification(Notification.Builder notificationBuilder, + Context context, String title, String summaryText, long startMillis, long endMillis, + long eventId, int notificationId, boolean doPopup, int priority, + boolean addActionButtons) { Resources resources = context.getResources(); if (title == null || title.length() == 0) { title = resources.getString(R.string.no_title_label); @@ -243,41 +245,74 @@ public class AlertReceiver extends BroadcastReceiver { endMillis, notificationId); // Create the base notification. - Notification.Builder notificationBuilder = new Notification.Builder(context); notificationBuilder.setContentTitle(title); notificationBuilder.setContentText(summaryText); notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar); notificationBuilder.setContentIntent(clickIntent); notificationBuilder.setDeleteIntent(deleteIntent); + if (doPopup) { + notificationBuilder.setFullScreenIntent(createAlertActivityIntent(context), true); + } + + PendingIntent snoozeIntent = null; + PendingIntent emailIntent = null; if (addActionButtons) { - // Create a snooze button. TODO: change snooze to 10 minutes. - PendingIntent snoozeIntent = createSnoozeIntent(context, eventId, startMillis, - endMillis, notificationId); - notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark, - resources.getString(R.string.snooze_label), snoozeIntent); - - // Create an email button. - PendingIntent emailIntent = createBroadcastMailIntent(context, eventId, title); + // Create snooze intent. TODO: change snooze to 10 minutes. + snoozeIntent = createSnoozeIntent(context, eventId, startMillis, endMillis, + notificationId); + + // Create email intent for emailing attendees. + emailIntent = createBroadcastMailIntent(context, eventId, title); + } + + if (Utils.isJellybeanOrLater()) { + // Turn off timestamp. + notificationBuilder.setWhen(0); + + // Should be one of the values in Notification (ie. Notification.PRIORITY_HIGH, etc). + // A higher priority will encourage notification manager to expand it. + notificationBuilder.setPriority(priority); + + // Add action buttons. + if (snoozeIntent != null) { + notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark, + resources.getString(R.string.snooze_label), snoozeIntent); + } if (emailIntent != null) { notificationBuilder.addAction(R.drawable.ic_menu_email_holo_dark, resources.getString(R.string.email_guests_label), emailIntent); } - } - if (doPopup) { - notificationBuilder.setFullScreenIntent(createAlertActivityIntent(context), true); - } + return notificationBuilder.getNotification(); - // Turn off timestamp. - notificationBuilder.setWhen(0); - - // Setting to a higher priority will encourage notification manager to expand the - // notification. - if (highPriority) { - notificationBuilder.setPriority(Notification.PRIORITY_HIGH); } else { - notificationBuilder.setPriority(Notification.PRIORITY_DEFAULT); + // Old-style notification (pre-JB). Use custom view with buttons to provide + // JB-like functionality (snooze/email). + Notification n = notificationBuilder.getNotification(); + + // Use custom view with buttons to provide JB-like functionality (snooze/email). + RemoteViews contentView = new RemoteViews(context.getPackageName(), + R.layout.notification); + contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar); + contentView.setTextViewText(R.id.title, title); + contentView.setTextViewText(R.id.text, summaryText); + if (snoozeIntent == null) { + contentView.setViewVisibility(R.id.email_button, View.GONE); + } else { + contentView.setViewVisibility(R.id.snooze_button, View.VISIBLE); + contentView.setOnClickPendingIntent(R.id.snooze_button, snoozeIntent); + contentView.setViewVisibility(R.id.end_padding, View.GONE); + } + if (emailIntent == null) { + contentView.setViewVisibility(R.id.email_button, View.GONE); + } else { + contentView.setViewVisibility(R.id.email_button, View.VISIBLE); + contentView.setOnClickPendingIntent(R.id.email_button, emailIntent); + contentView.setViewVisibility(R.id.end_padding, View.GONE); + } + n.contentView = contentView; + + return n; } - return notificationBuilder; } /** @@ -286,34 +321,36 @@ public class AlertReceiver extends BroadcastReceiver { */ public static NotificationWrapper makeExpandingNotification(Context context, String title, String summaryText, String description, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup, boolean highPriority) { - Notification.Builder basicBuilder = makeBasicNotificationBuilder(context, title, - summaryText, startMillis, endMillis, eventId, notificationId, - doPopup, highPriority, true); - - // Create an expanded notification - Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle( - basicBuilder); - if (description != null) { - description = mBlankLinePattern.matcher(description).replaceAll(""); - description = description.trim(); - } - CharSequence text; - if (TextUtils.isEmpty(description)) { - text = summaryText; - } else { - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - stringBuilder.append(summaryText); - stringBuilder.append("\n\n"); - stringBuilder.setSpan(new RelativeSizeSpan(0.5f), summaryText.length(), - stringBuilder.length(), 0); - stringBuilder.append(description); - text = stringBuilder; + int notificationId, boolean doPopup, int priority) { + Notification.Builder basicBuilder = new Notification.Builder(context); + Notification notification = buildBasicNotification(basicBuilder, context, title, + summaryText, startMillis, endMillis, eventId, notificationId, doPopup, + priority, true); + if (Utils.isJellybeanOrLater()) { + // Create a new-style expanded notification + Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle( + basicBuilder); + if (description != null) { + description = mBlankLinePattern.matcher(description).replaceAll(""); + description = description.trim(); + } + CharSequence text; + if (TextUtils.isEmpty(description)) { + text = summaryText; + } else { + SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); + stringBuilder.append(summaryText); + stringBuilder.append("\n\n"); + stringBuilder.setSpan(new RelativeSizeSpan(0.5f), summaryText.length(), + stringBuilder.length(), 0); + stringBuilder.append(description); + text = stringBuilder; + } + expandedBuilder.bigText(text); + notification = expandedBuilder.build(); } - expandedBuilder.bigText(text); - - return new NotificationWrapper(expandedBuilder.build(), notificationId, eventId, - startMillis, endMillis, doPopup); + return new NotificationWrapper(notification, notificationId, eventId, startMillis, + endMillis, doPopup); } /** @@ -357,63 +394,86 @@ public class AlertReceiver extends BroadcastReceiver { String nEventsStr = res.getQuantityString(R.plurals.Nevents, numEvents, numEvents); notificationBuilder.setContentTitle(nEventsStr); - // Set to min priority to encourage the notification manager to collapse it. - notificationBuilder.setPriority(Notification.PRIORITY_MIN); - Notification n; - - if (expandable) { - // Multiple reminders. Combine into an expanded digest notification. - Notification.InboxStyle expandedBuilder = new Notification.InboxStyle( - notificationBuilder); - int i = 0; - for (AlertService.NotificationInfo info : notificationInfos) { - if (i < NOTIFICATION_DIGEST_MAX_LENGTH) { - String name = info.eventName; - if (TextUtils.isEmpty(name)) { - name = context.getResources().getString(R.string.no_title_label); + if (Utils.isJellybeanOrLater()) { + // New-style notification... + + // Set to min priority to encourage the notification manager to collapse it. + notificationBuilder.setPriority(Notification.PRIORITY_MIN); + + if (expandable) { + // Multiple reminders. Combine into an expanded digest notification. + Notification.InboxStyle expandedBuilder = new Notification.InboxStyle( + notificationBuilder); + int i = 0; + for (AlertService.NotificationInfo info : notificationInfos) { + if (i < NOTIFICATION_DIGEST_MAX_LENGTH) { + String name = info.eventName; + if (TextUtils.isEmpty(name)) { + name = context.getResources().getString(R.string.no_title_label); + } + String timeLocation = AlertUtils.formatTimeLocation(context, + info.startMillis, info.allDay, info.location); + + TextAppearanceSpan primaryTextSpan = new TextAppearanceSpan(context, + R.style.NotificationPrimaryText); + TextAppearanceSpan secondaryTextSpan = new TextAppearanceSpan(context, + R.style.NotificationSecondaryText); + + // Event title in bold. + SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); + stringBuilder.append(name); + stringBuilder.setSpan(primaryTextSpan, 0, stringBuilder.length(), 0); + stringBuilder.append(" "); + + // Followed by time and location. + int secondaryIndex = stringBuilder.length(); + stringBuilder.append(timeLocation); + stringBuilder.setSpan(secondaryTextSpan, secondaryIndex, + stringBuilder.length(), 0); + expandedBuilder.addLine(stringBuilder); + i++; + } else { + break; } - String timeLocation = AlertUtils.formatTimeLocation(context, info.startMillis, - info.allDay, info.location); - - TextAppearanceSpan primaryTextSpan = new TextAppearanceSpan(context, - R.style.NotificationPrimaryText); - TextAppearanceSpan secondaryTextSpan = new TextAppearanceSpan(context, - R.style.NotificationSecondaryText); - - // Event title in bold. - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - stringBuilder.append(name); - stringBuilder.setSpan(primaryTextSpan, 0, stringBuilder.length(), 0); - stringBuilder.append(" "); - - // Followed by time and location. - int secondaryIndex = stringBuilder.length(); - stringBuilder.append(timeLocation); - stringBuilder.setSpan(secondaryTextSpan, secondaryIndex, stringBuilder.length(), - 0); - expandedBuilder.addLine(stringBuilder); - i++; - } else { - break; } - } - // If there are too many to display, add "+X missed events" for the last line. - int remaining = numEvents - i; - if (remaining > 0) { - String nMoreEventsStr = res.getQuantityString(R.plurals.N_remaining_events, - remaining, remaining); - // TODO: Add highlighting and icon to this last entry once framework allows it. - expandedBuilder.setSummaryText(nMoreEventsStr); - } + // If there are too many to display, add "+X missed events" for the last line. + int remaining = numEvents - i; + if (remaining > 0) { + String nMoreEventsStr = res.getQuantityString(R.plurals.N_remaining_events, + remaining, remaining); + // TODO: Add highlighting and icon to this last entry once framework allows it. + expandedBuilder.setSummaryText(nMoreEventsStr); + } - // Remove the title in the expanded form (redundant with the listed items). - expandedBuilder.setBigContentTitle(""); + // Remove the title in the expanded form (redundant with the listed items). + expandedBuilder.setBigContentTitle(""); - n = expandedBuilder.build(); + n = expandedBuilder.build(); + } else { + n = notificationBuilder.build(); + } } else { - n = notificationBuilder.build(); + // Old-style notification (pre-JB). We only need a standard notification (no + // buttons) but use a custom view so it is consistent with the others. + n = notificationBuilder.getNotification(); + + // Use custom view with buttons to provide JB-like functionality (snooze/email). + RemoteViews contentView = new RemoteViews(context.getPackageName(), + R.layout.notification); + contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar_multiple); + contentView.setTextViewText(R.id.title, nEventsStr); + contentView.setTextViewText(R.id.text, digestTitle); + contentView.setViewVisibility(R.id.time, View.VISIBLE); + contentView.setViewVisibility(R.id.email_button, View.GONE); + contentView.setViewVisibility(R.id.snooze_button, View.GONE); + contentView.setViewVisibility(R.id.end_padding, View.VISIBLE); + n.contentView = contentView; + + // Use timestamp to force expired digest notification to the bottom (there is no + // priority setting before JB release). This is hidden by the custom view. + n.when = 1; } NotificationWrapper nw = new NotificationWrapper(n); @@ -440,10 +500,12 @@ public class AlertReceiver extends BroadcastReceiver { Calendars.OWNER_ACCOUNT, // 0 Calendars.ACCOUNT_NAME, // 1 Events.TITLE, // 2 + Events.ORGANIZER, // 3 }; private static final int EVENT_INDEX_OWNER_ACCOUNT = 0; private static final int EVENT_INDEX_ACCOUNT_NAME = 1; private static final int EVENT_INDEX_TITLE = 2; + private static final int EVENT_INDEX_ORGANIZER = 3; private static Cursor getEventCursor(Context context, long eventId) { return context.getContentResolver().query( @@ -517,12 +579,14 @@ public class AlertReceiver extends BroadcastReceiver { String ownerAccount = null; String syncAccount = null; String eventTitle = null; + String eventOrganizer = null; Cursor eventCursor = getEventCursor(context, eventId); try { if (eventCursor != null && eventCursor.moveToFirst()) { ownerAccount = eventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT); syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME); eventTitle = eventCursor.getString(EVENT_INDEX_TITLE); + eventOrganizer = eventCursor.getString(EVENT_INDEX_ORGANIZER); } } finally { if (eventCursor != null) { @@ -557,6 +621,12 @@ public class AlertReceiver extends BroadcastReceiver { } } + // Add organizer only if no attendees to email (the case when too many attendees + // in the event to sync or show). + if (toEmails.size() == 0 && ccEmails.size() == 0 && eventOrganizer != null) { + addIfEmailable(toEmails, eventOrganizer, syncAccount); + } + Intent intent = null; if (ownerAccount != null && (toEmails.size() > 0 || ccEmails.size() > 0)) { intent = Utils.createEmailAttendeesIntent(context.getResources(), eventTitle, body, diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java index 897e9bde..17cdce82 100644 --- a/src/com/android/calendar/alerts/AlertService.java +++ b/src/com/android/calendar/alerts/AlertService.java @@ -16,7 +16,6 @@ package com.android.calendar.alerts; -import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; @@ -144,7 +143,7 @@ public class AlertService extends Service { } // Added wrapper for testing - public static class NotificationMgrWrapper implements NotificationMgr { + public static class NotificationMgrWrapper extends NotificationMgr { NotificationManager mNm; public NotificationMgrWrapper(NotificationManager nm) { @@ -157,24 +156,9 @@ public class AlertService extends Service { } @Override - public void cancel(String tag, int id) { - mNm.cancel(tag, id); - } - - @Override - public void cancelAll() { - mNm.cancelAll(); - } - - @Override public void notify(int id, NotificationWrapper nw) { mNm.notify(id, nw.mNotification); } - - @Override - public void notify(String tag, int id, NotificationWrapper nw) { - mNm.notify(tag, id, nw.mNotification); - } } void processMessage(Message msg) { @@ -248,12 +232,13 @@ public class AlertService extends Service { return false; } - return generateAlerts(context, nm, prefs, alertCursor, currentTime, MAX_NOTIFICATIONS); + return generateAlerts(context, nm, AlertUtils.createAlarmManager(context), prefs, + alertCursor, currentTime, MAX_NOTIFICATIONS); } public static boolean generateAlerts(Context context, NotificationMgr nm, - SharedPreferences prefs, Cursor alertCursor, final long currentTime, - final int maxNotifications) { + AlarmManagerInterface alarmMgr, SharedPreferences prefs, Cursor alertCursor, + final long currentTime, final int maxNotifications) { if (DEBUG) { Log.d(TAG, "alertCursor count:" + alertCursor.getCount()); } @@ -326,7 +311,8 @@ public class AlertService extends Service { info.allDay, info.location); notification = AlertReceiver.makeBasicNotification(context, info.eventName, summaryText, info.startMillis, info.endMillis, info.eventId, - AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false); + AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false, + Notification.PRIORITY_MIN); } else { // Multiple expired events are listed in a digest. notification = AlertReceiver.makeDigestNotification(context, @@ -354,9 +340,7 @@ public class AlertService extends Service { // Remove the notifications that are hanging around from the previous refresh. if (currentNotificationId <= maxNotifications) { - for (int i = currentNotificationId; i <= maxNotifications; i++) { - nm.cancel(i); - } + nm.cancelAllBetween(currentNotificationId, maxNotifications); if (DEBUG) { Log.d(TAG, "Canceling leftover notification IDs " + currentNotificationId + "-" + maxNotifications); @@ -366,7 +350,7 @@ public class AlertService extends Service { // Schedule the next silent refresh time so notifications will change // buckets (eg. drop into expired digest, etc). if (nextRefreshTime < Long.MAX_VALUE && nextRefreshTime > currentTime) { - AlertUtils.scheduleNextNotificationRefresh(context, null, nextRefreshTime); + AlertUtils.scheduleNextNotificationRefresh(context, alarmMgr, nextRefreshTime); if (DEBUG) { long minutesBeforeRefresh = (nextRefreshTime - currentTime) / MINUTE_MS; Time time = new Time(); @@ -379,6 +363,9 @@ public class AlertService extends Service { Log.e(TAG, "Illegal state: next notification refresh time found to be in the past."); } + // Flushes old fired alerts from internal storage, if needed. + AlertUtils.flushOldAlertsFromInternalStorage(context); + return true; } @@ -455,18 +442,27 @@ public class AlertService extends Service { } private static long getNextRefreshTime(NotificationInfo info, long currentTime) { - // We change an event's priority bucket at 15 minutes into the event (so recently started - // concurrent events stay high priority) + long startAdjustedForAllDay = info.startMillis; + long endAdjustedForAllDay = info.endMillis; + if (info.allDay) { + Time t = new Time(); + startAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, + Time.getCurrentTimezone()); + endAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, + Time.getCurrentTimezone()); + } + + // We change an event's priority bucket at 15 minutes into the event or 1/4 event duration. long nextRefreshTime = Long.MAX_VALUE; - long gracePeriodCutoff = info.startMillis + - getGracePeriodMs(info.startMillis, info.endMillis); + long gracePeriodCutoff = startAdjustedForAllDay + + getGracePeriodMs(startAdjustedForAllDay, endAdjustedForAllDay, info.allDay); if (gracePeriodCutoff > currentTime) { nextRefreshTime = Math.min(nextRefreshTime, gracePeriodCutoff); } // ... and at the end (so expiring ones drop into a digest). - if (info.endMillis > currentTime && info.endMillis > gracePeriodCutoff) { - nextRefreshTime = Math.min(nextRefreshTime, info.endMillis); + if (endAdjustedForAllDay > currentTime && endAdjustedForAllDay > gracePeriodCutoff) { + nextRefreshTime = Math.min(nextRefreshTime, endAdjustedForAllDay); } return nextRefreshTime; } @@ -507,11 +503,37 @@ public class AlertService extends Service { int state = alertCursor.getInt(ALERT_INDEX_STATE); final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0; + // Use app local storage to keep track of fired alerts to fix problem of multiple + // installed calendar apps potentially causing missed alarms. + boolean newAlertOverride = false; + String alertIdStr = Long.toString(alertId); + if (AlertUtils.BYPASS_DB && ((currentTime - alarmTime) / MINUTE_MS < 1)) { + // To avoid re-firing alerts, only fire if alarmTime is very recent. Otherwise + // we can get refires for non-dismissed alerts after app installation, or if the + // SharedPrefs was cleared too early. This means alerts that were timed while + // the phone was off may show up silently in the notification bar. + boolean alreadyFired = AlertUtils.hasAlertFiredInSharedPrefs(context, eventId, + beginTime, alarmTime); + if (!alreadyFired) { + newAlertOverride = true; + } + } + if (DEBUG) { - Log.d(TAG, "alertCursor result: alarmTime:" + alarmTime + " alertId:" + alertId - + " eventId:" + eventId + " state: " + state + " minutes:" + minutes - + " declined:" + declined + " beginTime:" + beginTime - + " endTime:" + endTime + " allDay:" + allDay); + StringBuilder msgBuilder = new StringBuilder(); + msgBuilder.append("alertCursor result: alarmTime:").append(alarmTime) + .append(" alertId:").append(alertId) + .append(" eventId:").append(eventId) + .append(" state: ").append(state) + .append(" minutes:").append(minutes) + .append(" declined:").append(declined) + .append(" beginTime:").append(beginTime) + .append(" endTime:").append(endTime) + .append(" allDay:").append(allDay); + if (AlertUtils.BYPASS_DB) { + msgBuilder.append(" newAlertOverride: " + newAlertOverride); + } + Log.d(TAG, msgBuilder.toString()); } ContentValues values = new ContentValues(); @@ -527,7 +549,7 @@ public class AlertService extends Service { // Remove declined events if (!declined) { - if (state == CalendarAlerts.STATE_SCHEDULED) { + if (state == CalendarAlerts.STATE_SCHEDULED || newAlertOverride) { newState = CalendarAlerts.STATE_FIRED; numFired++; newAlert = true; @@ -545,6 +567,11 @@ public class AlertService extends Service { if (newState != -1) { values.put(CalendarAlerts.STATE, newState); state = newState; + + if (AlertUtils.BYPASS_DB) { + AlertUtils.setAlertFiredInSharedPrefs(context, eventId, beginTime, + alarmTime); + } } if (state == CalendarAlerts.STATE_FIRED) { @@ -578,16 +605,12 @@ public class AlertService extends Service { // Adjust for all day events to ensure the right bucket. Don't use the 1/4 event // duration grace period for these. - long gracePeriodMs; long beginTimeAdjustedForAllDay = beginTime; String tz = null; if (allDay) { tz = TimeZone.getDefault().getID(); beginTimeAdjustedForAllDay = Utils.convertAlldayUtcToLocal(null, beginTime, tz); - gracePeriodMs = MIN_DEPRIORITIZE_GRACE_PERIOD_MS; - } else { - gracePeriodMs = getGracePeriodMs(beginTime, endTime); } // Handle multiple alerts for the same event ID. @@ -636,7 +659,8 @@ public class AlertService extends Service { // TODO: Prioritize by "primary" calendar eventIds.put(eventId, newInfo); - long highPriorityCutoff = currentTime - gracePeriodMs; + long highPriorityCutoff = currentTime - + getGracePeriodMs(beginTime, endTime, allDay); if (beginTimeAdjustedForAllDay > highPriorityCutoff) { // High priority = future events or events that just started @@ -659,8 +683,14 @@ public class AlertService extends Service { /** * High priority cutoff should be 1/4 event duration or 15 min, whichever is longer. */ - private static long getGracePeriodMs(long beginTime, long endTime) { - return Math.max(MIN_DEPRIORITIZE_GRACE_PERIOD_MS, ((endTime - beginTime) / 4)); + private static long getGracePeriodMs(long beginTime, long endTime, boolean allDay) { + if (allDay) { + // We don't want all day events to be high priority for hours, so automatically + // demote these after 15 min. + return MIN_DEPRIORITIZE_GRACE_PERIOD_MS; + } else { + return Math.max(MIN_DEPRIORITIZE_GRACE_PERIOD_MS, ((endTime - beginTime) / 4)); + } } private static String getDigestTitle(ArrayList<NotificationInfo> events) { @@ -679,11 +709,15 @@ public class AlertService extends Service { private static void postNotification(NotificationInfo info, String summaryText, Context context, boolean highPriority, NotificationPrefs prefs, NotificationMgr notificationMgr, int notificationId) { + int priorityVal = Notification.PRIORITY_DEFAULT; + if (highPriority) { + priorityVal = Notification.PRIORITY_HIGH; + } + String tickerText = getTickerText(info.eventName, info.location); NotificationWrapper notification = AlertReceiver.makeExpandingNotification(context, info.eventName, summaryText, info.description, info.startMillis, - info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), - highPriority); + info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), priorityVal); boolean quietUpdate = true; String ringtone = NotificationPrefs.EMPTY_RINGTONE; @@ -854,10 +888,8 @@ public class AlertService extends Service { private void doTimeChanged() { ContentResolver cr = getContentResolver(); - Object service = getSystemService(Context.ALARM_SERVICE); - AlarmManager manager = (AlarmManager) service; // TODO Move this into Provider - rescheduleMissedAlarms(cr, this, manager); + rescheduleMissedAlarms(cr, this, AlertUtils.createAlarmManager(this)); updateAlertNotification(this); } @@ -886,8 +918,8 @@ public class AlertService extends Service { * @param context the Context * @param manager the AlarmManager */ - public static final void rescheduleMissedAlarms(ContentResolver cr, Context context, - AlarmManager manager) { + private static final void rescheduleMissedAlarms(ContentResolver cr, Context context, + AlarmManagerInterface manager) { // Get all the alerts that have been scheduled but have not fired // and should have fired by now and are not too old. long now = System.currentTimeMillis(); @@ -950,6 +982,9 @@ public class AlertService extends Service { mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); + + // Flushes old fired alerts from internal storage, if needed. + AlertUtils.flushOldAlertsFromInternalStorage(getApplication()); } @Override diff --git a/src/com/android/calendar/alerts/AlertUtils.java b/src/com/android/calendar/alerts/AlertUtils.java index 1777a2d4..f68f5224 100644 --- a/src/com/android/calendar/alerts/AlertUtils.java +++ b/src/com/android/calendar/alerts/AlertUtils.java @@ -22,6 +22,7 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.provider.CalendarContract; import android.provider.CalendarContract.CalendarAlerts; @@ -29,15 +30,19 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.text.format.Time; +import android.util.Log; import com.android.calendar.EventInfoActivity; import com.android.calendar.R; import com.android.calendar.Utils; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; public class AlertUtils { + private static final String TAG = "AlertUtils"; + static final boolean DEBUG = true; public static final long SNOOZE_DELAY = 5 * 60 * 1000L; @@ -52,6 +57,42 @@ public class AlertUtils { public static final String NOTIFICATION_ID_KEY = "notificationid"; public static final String EVENT_IDS_KEY = "eventids"; + // A flag for using local storage to save alert state instead of the alerts DB table. + // This allows the unbundled app to run alongside other calendar apps without eating + // alerts from other apps. + static boolean BYPASS_DB = true; + + // SharedPrefs table name for storing fired alerts. This prevents other installed + // Calendar apps from eating the alerts. + private static final String ALERTS_SHARED_PREFS_NAME = "calendar_alerts"; + + // Keyname prefix for the alerts data in SharedPrefs. The key will contain a combo + // of event ID, begin time, and alarm time. The value will be the fired time. + private static final String KEY_FIRED_ALERT_PREFIX = "preference_alert_"; + + // The last time the SharedPrefs was scanned and flushed of old alerts data. + private static final String KEY_LAST_FLUSH_TIME_MS = "preference_flushTimeMs"; + + // The # of days to save alert states in the shared prefs table, before flushing. This + // can be any value, since AlertService will also check for a recent alertTime before + // ringing the alert. + private static final int FLUSH_INTERVAL_DAYS = 1; + private static final int FLUSH_INTERVAL_MS = FLUSH_INTERVAL_DAYS * 24 * 60 * 60 * 1000; + + /** + * Creates an AlarmManagerInterface that wraps a real AlarmManager. The alarm code + * was abstracted to an interface to make it testable. + */ + public static AlarmManagerInterface createAlarmManager(Context context) { + final AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + return new AlarmManagerInterface() { + @Override + public void set(int type, long triggerAtMillis, PendingIntent operation) { + mgr.set(type, triggerAtMillis, operation); + } + }; + } + /** * Schedules an alarm intent with the system AlarmManager that will notify * listeners when a reminder should be fired. The provider will keep @@ -63,7 +104,8 @@ public class AlertUtils { * @param manager The AlarmManager to use or null * @param alarmTime The time to fire the intent in UTC millis since epoch */ - public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { + public static void scheduleAlarm(Context context, AlarmManagerInterface manager, + long alarmTime) { scheduleAlarmHelper(context, manager, alarmTime, false); } @@ -71,17 +113,13 @@ public class AlertUtils { * Schedules the next alarm to silently refresh the notifications. Note that if there * is a pending silent refresh alarm, it will be replaced with this one. */ - static void scheduleNextNotificationRefresh(Context context, AlarmManager manager, + static void scheduleNextNotificationRefresh(Context context, AlarmManagerInterface manager, long alarmTime) { scheduleAlarmHelper(context, manager, alarmTime, true); } - private static void scheduleAlarmHelper(Context context, AlarmManager manager, long alarmTime, - boolean quietUpdate) { - if (manager == null) { - manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - } - + private static void scheduleAlarmHelper(Context context, AlarmManagerInterface manager, + long alarmTime, boolean quietUpdate) { int alarmType = AlarmManager.RTC_WAKEUP; Intent intent = new Intent(CalendarContract.ACTION_EVENT_REMINDER); intent.setClass(context, AlertReceiver.class); @@ -184,4 +222,102 @@ public class AlertUtils { return i; } + public static SharedPreferences getFiredAlertsTable(Context context) { + return context.getSharedPreferences(ALERTS_SHARED_PREFS_NAME, Context.MODE_PRIVATE); + } + + private static String getFiredAlertsKey(long eventId, long beginTime, + long alarmTime) { + StringBuilder sb = new StringBuilder(KEY_FIRED_ALERT_PREFIX); + sb.append(eventId); + sb.append("_"); + sb.append(beginTime); + sb.append("_"); + sb.append(alarmTime); + return sb.toString(); + } + + /** + * Returns whether the SharedPrefs storage indicates we have fired the alert before. + */ + static boolean hasAlertFiredInSharedPrefs(Context context, long eventId, long beginTime, + long alarmTime) { + SharedPreferences prefs = getFiredAlertsTable(context); + return prefs.contains(getFiredAlertsKey(eventId, beginTime, alarmTime)); + } + + /** + * Store fired alert info in the SharedPrefs. + */ + static void setAlertFiredInSharedPrefs(Context context, long eventId, long beginTime, + long alarmTime) { + // Store alarm time as the value too so we don't have to parse all the keys to flush + // old alarms out of the table later. + SharedPreferences prefs = getFiredAlertsTable(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(getFiredAlertsKey(eventId, beginTime, alarmTime), alarmTime); + editor.apply(); + } + + /** + * Scans and flushes the internal storage of old alerts. Looks up the previous flush + * time in SharedPrefs, and performs the flush if overdue. Otherwise, no-op. + */ + static void flushOldAlertsFromInternalStorage(Context context) { + if (BYPASS_DB) { + SharedPreferences prefs = getFiredAlertsTable(context); + + // Only flush if it hasn't been done in a while. + long nowTime = System.currentTimeMillis(); + long lastFlushTimeMs = prefs.getLong(KEY_LAST_FLUSH_TIME_MS, 0); + if (nowTime - lastFlushTimeMs > FLUSH_INTERVAL_MS) { + if (DEBUG) { + Log.d(TAG, "Flushing old alerts from shared prefs table"); + } + + // Scan through all fired alert entries, removing old ones. + SharedPreferences.Editor editor = prefs.edit(); + Time timeObj = new Time(); + for (Map.Entry<String, ?> entry : prefs.getAll().entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (key.startsWith(KEY_FIRED_ALERT_PREFIX)) { + long alertTime; + if (value instanceof Long) { + alertTime = (Long) value; + } else { + // Should never occur. + Log.e(TAG,"SharedPrefs key " + key + " did not have Long value: " + + value); + continue; + } + + if (nowTime - alertTime >= FLUSH_INTERVAL_MS) { + editor.remove(key); + if (DEBUG) { + int ageInDays = getIntervalInDays(alertTime, nowTime, timeObj); + Log.d(TAG, "SharedPrefs key " + key + ": removed (" + ageInDays + + " days old)"); + } + } else { + if (DEBUG) { + int ageInDays = getIntervalInDays(alertTime, nowTime, timeObj); + Log.d(TAG, "SharedPrefs key " + key + ": keep (" + ageInDays + + " days old)"); + } + } + } + } + editor.putLong(KEY_LAST_FLUSH_TIME_MS, nowTime); + editor.apply(); + } + } + } + + private static int getIntervalInDays(long startMillis, long endMillis, Time timeObj) { + timeObj.set(startMillis); + int startDay = Time.getJulianDay(startMillis, timeObj.gmtoff); + timeObj.set(endMillis); + return Time.getJulianDay(endMillis, timeObj.gmtoff) - startDay; + } } diff --git a/src/com/android/calendar/alerts/DismissAlarmsService.java b/src/com/android/calendar/alerts/DismissAlarmsService.java index dc7de541..b52ffd5a 100644 --- a/src/com/android/calendar/alerts/DismissAlarmsService.java +++ b/src/com/android/calendar/alerts/DismissAlarmsService.java @@ -18,7 +18,6 @@ package com.android.calendar.alerts; import android.app.IntentService; import android.app.NotificationManager; -import android.app.TaskStackBuilder; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -26,6 +25,7 @@ import android.content.Intent; import android.net.Uri; import android.os.IBinder; import android.provider.CalendarContract.CalendarAlerts; +import android.support.v4.app.TaskStackBuilder; import com.android.calendar.EventInfoActivity; @@ -86,6 +86,7 @@ public class DismissAlarmsService extends IntentService { // Show event on Calendar app by building an intent and task stack to start // EventInfoActivity with AllInOneActivity as the parent activity rooted to home. Intent i = AlertUtils.buildEventViewIntent(this, eventId, eventStart, eventEnd); + TaskStackBuilder.create(this) .addParentStack(EventInfoActivity.class).addNextIntent(i).startActivities(); } diff --git a/src/com/android/calendar/alerts/NotificationMgr.java b/src/com/android/calendar/alerts/NotificationMgr.java index e0a5baff..b62f6064 100644 --- a/src/com/android/calendar/alerts/NotificationMgr.java +++ b/src/com/android/calendar/alerts/NotificationMgr.java @@ -2,10 +2,24 @@ package com.android.calendar.alerts; import com.android.calendar.alerts.AlertService.NotificationWrapper; -public interface NotificationMgr { - public void cancel(int id); - public void cancel(String tag, int id); - public void cancelAll(); - public void notify(int id, NotificationWrapper notification); - public void notify(String tag, int id, NotificationWrapper notification); +public abstract class NotificationMgr { + public abstract void notify(int id, NotificationWrapper notification); + public abstract void cancel(int id); + + /** + * Don't actually use the notification framework's cancelAll since the SyncAdapter + * might post notifications and we don't want to affect those. + */ + public void cancelAll() { + cancelAllBetween(0, AlertService.MAX_NOTIFICATIONS); + } + + /** + * Cancels IDs between the specified bounds, inclusively. + */ + public void cancelAllBetween(int from, int to) { + for (int i = from; i <= to; i++) { + cancel(i); + } + } } diff --git a/src/com/android/calendar/alerts/QuickResponseActivity.java b/src/com/android/calendar/alerts/QuickResponseActivity.java index eed1c55d..f7e08883 100644 --- a/src/com/android/calendar/alerts/QuickResponseActivity.java +++ b/src/com/android/calendar/alerts/QuickResponseActivity.java @@ -40,7 +40,7 @@ import java.util.Arrays; */ public class QuickResponseActivity extends ListActivity implements OnItemClickListener { private static final String TAG = "QuickResponseActivity"; - static final String EXTRA_EVENT_ID = "eventId"; + public static final String EXTRA_EVENT_ID = "eventId"; private String[] mResponses = null; static long mEventId; diff --git a/src/com/android/calendar/alerts/SnoozeAlarmsService.java b/src/com/android/calendar/alerts/SnoozeAlarmsService.java index 2eac1a71..6ef983d5 100644 --- a/src/com/android/calendar/alerts/SnoozeAlarmsService.java +++ b/src/com/android/calendar/alerts/SnoozeAlarmsService.java @@ -80,7 +80,8 @@ public class SnoozeAlarmsService extends IntentService { ContentValues values = AlertUtils.makeContentValues(eventId, eventStart, eventEnd, alarmTime, 0); resolver.insert(uri, values); - AlertUtils.scheduleAlarm(SnoozeAlarmsService.this, null, alarmTime); + AlertUtils.scheduleAlarm(SnoozeAlarmsService.this, AlertUtils.createAlarmManager(this), + alarmTime); } AlertService.updateAlertNotification(this); stopSelf(); diff --git a/src/com/android/calendar/event/EditEventFragment.java b/src/com/android/calendar/event/EditEventFragment.java index 310010eb..50b80528 100644 --- a/src/com/android/calendar/event/EditEventFragment.java +++ b/src/com/android/calendar/event/EditEventFragment.java @@ -510,33 +510,30 @@ public class EditEventFragment extends Fragment implements EventHandler { * @return whether the event was handled here */ private boolean onActionBarItemSelected(int itemId) { - switch (itemId) { - case R.id.action_done: - if (EditEventHelper.canModifyEvent(mModel) || EditEventHelper.canRespond(mModel)) { - if (mView != null && mView.prepareForSave()) { - if (mModification == Utils.MODIFY_UNINITIALIZED) { - mModification = Utils.MODIFY_ALL; - } - mOnDone.setDoneCode(Utils.DONE_SAVE | Utils.DONE_EXIT); - mOnDone.run(); - } else { - mOnDone.setDoneCode(Utils.DONE_REVERT); - mOnDone.run(); + if (itemId == R.id.action_done) { + if (EditEventHelper.canModifyEvent(mModel) || EditEventHelper.canRespond(mModel)) { + if (mView != null && mView.prepareForSave()) { + if (mModification == Utils.MODIFY_UNINITIALIZED) { + mModification = Utils.MODIFY_ALL; } - } else if (EditEventHelper.canAddReminders(mModel) && mModel.mId != -1 - && mOriginalModel != null && mView.prepareForSave()) { - saveReminders(); - mOnDone.setDoneCode(Utils.DONE_EXIT); + mOnDone.setDoneCode(Utils.DONE_SAVE | Utils.DONE_EXIT); mOnDone.run(); } else { mOnDone.setDoneCode(Utils.DONE_REVERT); mOnDone.run(); } - break; - case R.id.action_cancel: + } else if (EditEventHelper.canAddReminders(mModel) && mModel.mId != -1 + && mOriginalModel != null && mView.prepareForSave()) { + saveReminders(); + mOnDone.setDoneCode(Utils.DONE_EXIT); + mOnDone.run(); + } else { mOnDone.setDoneCode(Utils.DONE_REVERT); mOnDone.run(); - break; + } + } else if (itemId == R.id.action_cancel) { + mOnDone.setDoneCode(Utils.DONE_REVERT); + mOnDone.run(); } return true; } diff --git a/src/com/android/calendar/event/EditEventHelper.java b/src/com/android/calendar/event/EditEventHelper.java index 4a6ff5ce..03773fda 100644 --- a/src/com/android/calendar/event/EditEventHelper.java +++ b/src/com/android/calendar/event/EditEventHelper.java @@ -35,15 +35,16 @@ import android.text.util.Rfc822Tokenizer; import android.util.Log; import android.view.View; +import com.android.calendar.AbstractCalendarActivity; import com.android.calendar.AsyncQueryService; import com.android.calendar.CalendarEventModel; import com.android.calendar.CalendarEventModel.Attendee; import com.android.calendar.CalendarEventModel.ReminderEntry; import com.android.calendar.Utils; -import com.android.calendarcommon.DateException; -import com.android.calendarcommon.EventRecurrence; -import com.android.calendarcommon.RecurrenceProcessor; -import com.android.calendarcommon.RecurrenceSet; +import com.android.calendarcommon2.DateException; +import com.android.calendarcommon2.EventRecurrence; +import com.android.calendarcommon2.RecurrenceProcessor; +import com.android.calendarcommon2.RecurrenceSet; import com.android.common.Rfc822Validator; import java.util.ArrayList; @@ -168,6 +169,8 @@ public class EditEventHelper { Calendars.ALLOWED_REMINDERS, // 8 Calendars.ALLOWED_ATTENDEE_TYPES, // 9 Calendars.ALLOWED_AVAILABILITY, // 10 + Calendars.ACCOUNT_NAME, // 11 + Calendars.ACCOUNT_TYPE, //12 }; static final int CALENDARS_INDEX_ID = 0; static final int CALENDARS_INDEX_DISPLAY_NAME = 1; @@ -180,6 +183,8 @@ public class EditEventHelper { static final int CALENDARS_INDEX_ALLOWED_REMINDERS = 8; static final int CALENDARS_INDEX_ALLOWED_ATTENDEE_TYPES = 9; static final int CALENDARS_INDEX_ALLOWED_AVAILABILITY = 10; + static final int CALENDARS_INDEX_ACCOUNT_NAME = 11; + static final int CALENDARS_INDEX_ACCOUNT_TYPE = 12; static final String CALENDARS_WHERE_WRITEABLE_VISIBLE = Calendars.CALENDAR_ACCESS_LEVEL + ">=" + Calendars.CAL_ACCESS_CONTRIBUTOR + " AND " + Calendars.VISIBLE + "=1"; @@ -215,7 +220,7 @@ public class EditEventHelper { } public EditEventHelper(Context context, CalendarEventModel model) { - mService = new AsyncQueryService(context); + mService = ((AbstractCalendarActivity)context).getAsyncQueryService(); } /** diff --git a/src/com/android/calendar/event/EditEventView.java b/src/com/android/calendar/event/EditEventView.java index ca270237..3ef527e2 100644 --- a/src/com/android/calendar/event/EditEventView.java +++ b/src/com/android/calendar/event/EditEventView.java @@ -24,17 +24,20 @@ import android.app.ProgressDialog; import android.app.Service; import android.app.TimePickerDialog; import android.app.TimePickerDialog.OnTimeSetListener; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; +import android.database.MatrixCursor; import android.graphics.drawable.Drawable; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; +import android.provider.CalendarContract; import android.provider.Settings; import android.text.InputFilter; import android.text.TextUtils; @@ -43,14 +46,17 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.text.util.Rfc822Tokenizer; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CalendarView; import android.widget.CheckBox; @@ -64,6 +70,7 @@ import android.widget.ResourceCursorAdapter; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; import android.widget.TimePicker; import com.android.calendar.CalendarEventModel; @@ -78,7 +85,7 @@ import com.android.calendar.TimezoneAdapter; import com.android.calendar.TimezoneAdapter.TimezoneRow; import com.android.calendar.Utils; import com.android.calendar.event.EditEventHelper.EditDoneRunnable; -import com.android.calendarcommon.EventRecurrence; +import com.android.calendarcommon2.EventRecurrence; import com.android.common.Rfc822InputFilter; import com.android.common.Rfc822Validator; import com.android.ex.chips.AccountSpecifier; @@ -93,6 +100,7 @@ import java.util.Formatter; import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; +import java.util.TreeMap; public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener, DialogInterface.OnClickListener, OnItemSelectedListener { @@ -100,6 +108,16 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com"; private static final String PERIOD_SPACE = ". "; + // Constants used for title autocompletion. + private static final String[] EVENT_PROJECTION = new String[] { + Events._ID, + Events.TITLE, + }; + private static final int EVENT_INDEX_ID = 0; + private static final int EVENT_INDEX_TITLE = 1; + private static final String TITLE_WHERE = Events.TITLE + " LIKE ?"; + private static final int MAX_TITLE_SUGGESTIONS = 4; + ArrayList<View> mEditOnlyList = new ArrayList<View>(); ArrayList<View> mEditViewList = new ArrayList<View>(); ArrayList<View> mViewOnlyList = new ArrayList<View>(); @@ -121,7 +139,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa Spinner mAvailabilitySpinner; Spinner mAccessLevelSpinner; RadioGroup mResponseRadioGroup; - TextView mTitleTextView; + AutoCompleteTextView mTitleTextView; TextView mLocationTextView; TextView mDescriptionTextView; TextView mWhenView; @@ -627,6 +645,106 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa } /** + * Adapter for title auto completion. + */ + private static class TitleAdapter extends ResourceCursorAdapter { + private final ContentResolver mContentResolver; + + public TitleAdapter(Context context) { + super(context, android.R.layout.simple_dropdown_item_1line, null, 0); + mContentResolver = context.getContentResolver(); + } + + @Override + public int getCount() { + return Math.min(MAX_TITLE_SUGGESTIONS, super.getCount()); + } + + private static String getTitleAtCursor(Cursor cursor) { + return cursor.getString(EVENT_INDEX_TITLE); + } + + @Override + public final String convertToString(Cursor cursor) { + return getTitleAtCursor(cursor); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView textView = (TextView) view; + textView.setText(getTitleAtCursor(cursor)); + } + + @Override + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + String filter = constraint == null ? "" : constraint.toString() + "%"; + if (filter.isEmpty()) { + return null; + } + long startTime = System.currentTimeMillis(); + + // Query all titles prefixed with the constraint. There is no way to insert + // 'DISTINCT' or 'GROUP BY' to get rid of dupes, so use post-processing to + // remove dupes. We will order query results by descending event ID to show + // results that were most recently inputted. + Cursor tempCursor = mContentResolver.query(Events.CONTENT_URI, EVENT_PROJECTION, + TITLE_WHERE, new String[] { filter }, Events._ID + " DESC"); + if (tempCursor != null) { + try { + // Post process query results. + Cursor c = uniqueTitlesCursor(tempCursor); + + // Log the processing duration. + long duration = System.currentTimeMillis() - startTime; + StringBuilder msg = new StringBuilder(); + msg.append("Autocomplete of "); + msg.append(constraint); + msg.append(": title query match took "); + msg.append(duration); + msg.append("ms."); + Log.d(TAG, msg.toString()); + return c; + } finally { + tempCursor.close(); + } + } else { + return null; + } + } + + /** + * Post-process the query results to return the first MAX_TITLE_SUGGESTIONS + * unique titles in alphabetical order. + */ + private Cursor uniqueTitlesCursor(Cursor cursor) { + TreeMap<String, String[]> titleToQueryResults = + new TreeMap<String, String[]>(String.CASE_INSENSITIVE_ORDER); + int numColumns = cursor.getColumnCount(); + cursor.moveToPosition(-1); + + // Remove dupes. + while ((titleToQueryResults.size() < MAX_TITLE_SUGGESTIONS) && cursor.moveToNext()) { + String title = getTitleAtCursor(cursor).trim(); + String data[] = new String[numColumns]; + if (!titleToQueryResults.containsKey(title)) { + for (int i = 0; i < numColumns; i++) { + data[i] = cursor.getString(i); + } + titleToQueryResults.put(title, data); + } + } + + // Copy the sorted results to a new cursor. + MatrixCursor newCursor = new MatrixCursor(EVENT_PROJECTION); + for (String[] result : titleToQueryResults.values()) { + newCursor.addRow(result); + } + newCursor.moveToFirst(); + return newCursor; + } + } + + /** * Does prep steps for saving a calendar event. * * This triggers a parse of the attendees list and checks if the event is @@ -829,7 +947,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa // cache all the widgets mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner); - mTitleTextView = (TextView) view.findViewById(R.id.title); + mTitleTextView = (AutoCompleteTextView) view.findViewById(R.id.title); mLocationTextView = (TextView) view.findViewById(R.id.location); mDescriptionTextView = (TextView) view.findViewById(R.id.description); mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label); @@ -863,6 +981,19 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees); mTitleTextView.setTag(mTitleTextView.getBackground()); + mTitleTextView.setAdapter(new TitleAdapter(activity)); + mTitleTextView.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + // Dismiss the suggestions dropdown. Return false so the other + // side effects still occur (soft keyboard going away, etc.). + mTitleTextView.dismissDropDown(); + } + return false; + } + }); + mLocationTextView.setTag(mLocationTextView.getBackground()); mDescriptionTextView.setTag(mDescriptionTextView.getBackground()); mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground()); @@ -1415,14 +1546,24 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa String defaultCalendar = Utils.getSharedPreference( mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, (String) null); - if (defaultCalendar == null) { - return 0; - } int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); + int accountNameIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME); + int accountTypeIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE); int position = 0; calendarsCursor.moveToPosition(-1); while (calendarsCursor.moveToNext()) { - if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) { + String calendarOwner = calendarsCursor.getString(calendarsOwnerColumn); + if (defaultCalendar == null) { + // There is no stored default upon the first time running. Use a primary + // calendar in this case. + if (calendarOwner != null && + calendarOwner.equals(calendarsCursor.getString(accountNameIndex)) && + !CalendarContract.ACCOUNT_TYPE_LOCAL.equals( + calendarsCursor.getString(accountTypeIndex))) { + return position; + } + } else if (defaultCalendar.equals(calendarOwner)) { + // Found the default calendar. return position; } position++; diff --git a/src/com/android/calendar/month/MonthByWeekAdapter.java b/src/com/android/calendar/month/MonthByWeekAdapter.java index c08147ad..8d30adc8 100644 --- a/src/com/android/calendar/month/MonthByWeekAdapter.java +++ b/src/com/android/calendar/month/MonthByWeekAdapter.java @@ -286,7 +286,7 @@ public class MonthByWeekAdapter extends SimpleWeeksAdapter { day.minute = currTime.minute; day.allDay = false; day.normalize(true); - if (mShowAgendaWithMonth && !mIsMiniMonth) { + if (mShowAgendaWithMonth || mIsMiniMonth) { // If agenda view is visible with month view , refresh the views // with the selected day's info mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, @@ -294,13 +294,12 @@ public class MonthByWeekAdapter extends SimpleWeeksAdapter { } else { // Else , switch to the detailed view mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, - mIsMiniMonth ? ViewType.CURRENT : ViewType.DETAIL, - CalendarController.EXTRA_GOTO_DATE + ViewType.DETAIL, + CalendarController.EXTRA_GOTO_DATE | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); } } - @Override public boolean onTouch(View v, MotionEvent event) { if (!(v instanceof MonthWeekEventsView)) { diff --git a/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountActivity.java b/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountActivity.java index 9ec8f9c8..a4e628c1 100644 --- a/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountActivity.java +++ b/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountActivity.java @@ -16,9 +16,6 @@ package com.android.calendar.selectcalendars; -import com.android.calendar.R; -import com.android.calendar.Utils; - import android.app.ActionBar; import android.app.ExpandableListActivity; import android.content.ContentResolver; @@ -31,6 +28,9 @@ import android.view.MenuItem; import android.view.View; import android.widget.ExpandableListView; +import com.android.calendar.R; +import com.android.calendar.Utils; + public class SelectSyncedCalendarsMultiAccountActivity extends ExpandableListActivity implements View.OnClickListener { @@ -60,7 +60,9 @@ public class SelectSyncedCalendarsMultiAccountActivity extends ExpandableListAct null /* selectionArgs */, Calendars.ACCOUNT_NAME /*sort order*/); MatrixCursor accountsCursor = Utils.matrixCursorFromCursor(mCursor); - startManagingCursor(accountsCursor); + if (accountsCursor != null) { + startManagingCursor(accountsCursor); + } mAdapter = new SelectSyncedCalendarsMultiAccountAdapter(findViewById(R.id.calendars) .getContext(), accountsCursor, this); @@ -81,15 +83,11 @@ public class SelectSyncedCalendarsMultiAccountActivity extends ExpandableListAct @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.btn_done: - mAdapter.doSaveAction(); - finish(); - break; - - case R.id.btn_discard: - finish(); - break; + if (view.getId() == R.id.btn_done) { + mAdapter.doSaveAction(); + finish(); + } else if (view.getId() == R.id.btn_discard) { + finish(); } } diff --git a/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountAdapter.java b/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountAdapter.java index 58b99ebf..77a8da72 100644 --- a/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountAdapter.java +++ b/src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountAdapter.java @@ -88,7 +88,6 @@ public class SelectSyncedCalendarsMultiAccountAdapter extends CursorTreeAdapter // How long to keep refreshing for private static final int REFRESH_DURATION = 60000; private static boolean mRefresh = true; - private int mNumAccounts; private static String mSyncedText; private static String mNotSyncedText; @@ -204,11 +203,8 @@ public class SelectSyncedCalendarsMultiAccountAdapter extends CursorTreeAdapter mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver); } - mNumAccounts = acctsCursor.getCount(); - if (mNumAccounts == 0) { - // Should never happen since Calendar requires an account exist to - // use it. - Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!"); + if (acctsCursor == null || acctsCursor.getCount() == 0) { + Log.i(TAG, "SelectCalendarsAdapter: No accounts were returned!"); } // Collect proper description for account types mAuthDescs = AccountManager.get(context).getAuthenticatorTypes(); diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.java b/src/com/android/calendar/widget/CalendarAppWidgetService.java index 00b41631..b3a5dc92 100644 --- a/src/com/android/calendar/widget/CalendarAppWidgetService.java +++ b/src/com/android/calendar/widget/CalendarAppWidgetService.java @@ -75,7 +75,7 @@ public class CalendarAppWidgetService extends RemoteViewsService { Instances.EVENT_ID, Instances.START_DAY, Instances.END_DAY, - Instances.DISPLAY_COLOR, + Instances.DISPLAY_COLOR, // If SDK < 16, set to Instances.CALENDAR_COLOR. Instances.SELF_ATTENDEE_STATUS, }; @@ -90,6 +90,11 @@ public class CalendarAppWidgetService extends RemoteViewsService { static final int INDEX_COLOR = 8; static final int INDEX_SELF_ATTENDEE_STATUS = 9; + static { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR; + } + } static final int MAX_DAYS = 7; private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS; |