summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSteve Kondik <shade@chemlab.org>2012-11-18 22:37:21 -0800
committerSteve Kondik <shade@chemlab.org>2012-11-18 22:37:21 -0800
commit3905962fd7d1cf204d8fb5ea3c7c709814faefc7 (patch)
tree5593373426fa3806f4ebf5d4fbeb7ed47d484c45 /src
parented8b229028feab713d38d47023e5d5729d456fcc (diff)
parent898d0e6ac26d5fbcbe8a57d46acea4200ae1aeac (diff)
downloadandroid_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')
-rw-r--r--src/com/android/calendar/AboutPreferences.java42
-rw-r--r--src/com/android/calendar/AllInOneActivity.java138
-rw-r--r--src/com/android/calendar/CalendarApplication.java5
-rw-r--r--src/com/android/calendar/CalendarSettingsActivity.java21
-rw-r--r--src/com/android/calendar/DayView.java11
-rw-r--r--src/com/android/calendar/DeleteEventHelper.java2
-rw-r--r--src/com/android/calendar/Event.java8
-rw-r--r--src/com/android/calendar/EventInfoActivity.java34
-rw-r--r--src/com/android/calendar/EventInfoFragment.java322
-rw-r--r--src/com/android/calendar/EventRecurrenceFormatter.java2
-rw-r--r--src/com/android/calendar/GeneralPreferences.java1
-rw-r--r--src/com/android/calendar/GoogleCalendarUriIntentFilter.java4
-rw-r--r--src/com/android/calendar/SearchActivity.java42
-rw-r--r--src/com/android/calendar/Utils.java45
-rw-r--r--src/com/android/calendar/agenda/AgendaByDayAdapter.java30
-rw-r--r--src/com/android/calendar/agenda/AgendaFragment.java68
-rw-r--r--src/com/android/calendar/agenda/AgendaListView.java67
-rw-r--r--src/com/android/calendar/agenda/AgendaWindowAdapter.java38
-rw-r--r--src/com/android/calendar/alerts/AlarmManagerInterface.java10
-rw-r--r--src/com/android/calendar/alerts/AlertActivity.java31
-rw-r--r--src/com/android/calendar/alerts/AlertAdapter.java14
-rw-r--r--src/com/android/calendar/alerts/AlertReceiver.java284
-rw-r--r--src/com/android/calendar/alerts/AlertService.java135
-rw-r--r--src/com/android/calendar/alerts/AlertUtils.java152
-rw-r--r--src/com/android/calendar/alerts/DismissAlarmsService.java3
-rw-r--r--src/com/android/calendar/alerts/NotificationMgr.java26
-rw-r--r--src/com/android/calendar/alerts/QuickResponseActivity.java2
-rw-r--r--src/com/android/calendar/alerts/SnoozeAlarmsService.java3
-rw-r--r--src/com/android/calendar/event/EditEventFragment.java35
-rw-r--r--src/com/android/calendar/event/EditEventHelper.java15
-rw-r--r--src/com/android/calendar/event/EditEventView.java155
-rw-r--r--src/com/android/calendar/month/MonthByWeekAdapter.java7
-rw-r--r--src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountActivity.java24
-rw-r--r--src/com/android/calendar/selectcalendars/SelectSyncedCalendarsMultiAccountAdapter.java8
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetService.java7
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;