1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calendar.alerts; 18 19 import com.android.calendar.AllInOneActivity; 20 import com.android.calendar.AsyncQueryService; 21 import com.android.calendar.R; 22 import com.android.calendar.Utils; 23 24 import android.app.Activity; 25 import android.app.AlarmManager; 26 import android.app.NotificationManager; 27 import android.app.PendingIntent; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.database.Cursor; 33 import android.net.Uri; 34 import android.net.Uri.Builder; 35 import android.os.Bundle; 36 import android.provider.CalendarContract; 37 import android.provider.CalendarContract.CalendarAlerts; 38 import android.util.Log; 39 import android.view.View; 40 import android.view.View.OnClickListener; 41 import android.widget.AdapterView; 42 import android.widget.AdapterView.OnItemClickListener; 43 import android.widget.Button; 44 import android.widget.ListView; 45 46 /** 47 * The alert panel that pops up when there is a calendar event alarm. 48 * This activity is started by an intent that specifies an event id. 49 */ 50 public class AlertActivity extends Activity implements OnClickListener { 51 private static final String TAG = "AlertActivity"; 52 53 // The default snooze delay: 5 minutes 54 public static final long SNOOZE_DELAY = 5 * 60 * 1000L; 55 56 private static final String[] PROJECTION = new String[] { 57 CalendarAlerts._ID, // 0 58 CalendarAlerts.TITLE, // 1 59 CalendarAlerts.EVENT_LOCATION, // 2 60 CalendarAlerts.ALL_DAY, // 3 61 CalendarAlerts.BEGIN, // 4 62 CalendarAlerts.END, // 5 63 CalendarAlerts.EVENT_ID, // 6 64 CalendarAlerts.CALENDAR_COLOR, // 7 65 CalendarAlerts.RRULE, // 8 66 CalendarAlerts.HAS_ALARM, // 9 67 CalendarAlerts.STATE, // 10 68 CalendarAlerts.ALARM_TIME, // 11 69 }; 70 71 public static final int INDEX_ROW_ID = 0; 72 public static final int INDEX_TITLE = 1; 73 public static final int INDEX_EVENT_LOCATION = 2; 74 public static final int INDEX_ALL_DAY = 3; 75 public static final int INDEX_BEGIN = 4; 76 public static final int INDEX_END = 5; 77 public static final int INDEX_EVENT_ID = 6; 78 public static final int INDEX_COLOR = 7; 79 public static final int INDEX_RRULE = 8; 80 public static final int INDEX_HAS_ALARM = 9; 81 public static final int INDEX_STATE = 10; 82 public static final int INDEX_ALARM_TIME = 11; 83 84 private static final String SELECTION = CalendarAlerts.STATE + "=?"; 85 private static final String[] SELECTIONARG = new String[] { 86 Integer.toString(CalendarAlerts.STATE_FIRED) 87 }; 88 89 // We use one notification id for all events so that we don't clutter 90 // the notification screen. It doesn't matter what the id is, as long 91 // as it is used consistently everywhere. 92 public static final int NOTIFICATION_ID = 0; 93 94 private AlertAdapter mAdapter; 95 private QueryHandler mQueryHandler; 96 private Cursor mCursor; 97 private ListView mListView; 98 private Button mSnoozeAllButton; 99 private Button mDismissAllButton; 100 101 102 private void dismissFiredAlarms() { 103 ContentValues values = new ContentValues(1 /* size */); 104 values.put(PROJECTION[INDEX_STATE], CalendarAlerts.STATE_DISMISSED); 105 String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED; 106 mQueryHandler.startUpdate(0, null, CalendarAlerts.CONTENT_URI, values, 107 selection, null /* selectionArgs */, Utils.UNDO_DELAY); 108 } 109 110 private void dismissAlarm(long id) { 111 ContentValues values = new ContentValues(1 /* size */); 112 values.put(PROJECTION[INDEX_STATE], CalendarAlerts.STATE_DISMISSED); 113 String selection = CalendarAlerts._ID + "=" + id; 114 mQueryHandler.startUpdate(0, null, CalendarAlerts.CONTENT_URI, values, 115 selection, null /* selectionArgs */, Utils.UNDO_DELAY); 116 } 117 118 private class QueryHandler extends AsyncQueryService { 119 public QueryHandler(Context context) { 120 super(context); 121 } 122 123 @Override 124 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 125 // Only set mCursor if the Activity is not finishing. Otherwise close the cursor. 126 if (!isFinishing()) { 127 mCursor = cursor; 128 mAdapter.changeCursor(cursor); 129 mListView.setSelection(cursor.getCount() - 1); 130 131 // The results are in, enable the buttons 132 mSnoozeAllButton.setEnabled(true); 133 mDismissAllButton.setEnabled(true); 134 } else { 135 cursor.close(); 136 } 137 } 138 139 @Override 140 protected void onInsertComplete(int token, Object cookie, Uri uri) { 141 if (uri != null) { 142 Long alarmTime = (Long) cookie; 143 144 if (alarmTime != 0) { 145 // Set a new alarm to go off after the snooze delay. 146 // TODO make provider schedule this automatically when 147 // inserting an alarm 148 AlarmManager alarmManager = 149 (AlarmManager) getSystemService(Context.ALARM_SERVICE); 150 scheduleAlarm(AlertActivity.this, alarmManager, alarmTime); 151 } 152 } 153 } 154 155 @Override 156 protected void onUpdateComplete(int token, Object cookie, int result) { 157 // Ignore 158 } 159 } 160 161 /** 162 * Schedules an alarm intent with the system AlarmManager that will notify 163 * listeners when a reminder should be fired. The provider will keep 164 * scheduled reminders up to date but apps may use this to implement snooze 165 * functionality without modifying the reminders table. Scheduled alarms 166 * will generate an intent using {@link #ACTION_EVENT_REMINDER}. 167 * 168 * @param context A context for referencing system resources 169 * @param manager The AlarmManager to use or null 170 * @param alarmTime The time to fire the intent in UTC millis since epoch 171 */ 172 public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { 173 174 if (manager == null) { 175 manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 176 } 177 178 Intent intent = new Intent(CalendarContract.ACTION_EVENT_REMINDER); 179 intent.setData(ContentUris.withAppendedId(CalendarContract.CONTENT_URI, alarmTime)); 180 intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); 181 PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0); 182 manager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); 183 } 184 185 private static ContentValues makeContentValues(long eventId, long begin, long end, 186 long alarmTime, int minutes) { 187 ContentValues values = new ContentValues(); 188 values.put(CalendarAlerts.EVENT_ID, eventId); 189 values.put(CalendarAlerts.BEGIN, begin); 190 values.put(CalendarAlerts.END, end); 191 values.put(CalendarAlerts.ALARM_TIME, alarmTime); 192 long currentTime = System.currentTimeMillis(); 193 values.put(CalendarAlerts.CREATION_TIME, currentTime); 194 values.put(CalendarAlerts.RECEIVED_TIME, 0); 195 values.put(CalendarAlerts.NOTIFY_TIME, 0); 196 values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_SCHEDULED); 197 values.put(CalendarAlerts.MINUTES, minutes); 198 return values; 199 } 200 201 private OnItemClickListener mViewListener = new OnItemClickListener() { 202 203 public void onItemClick(AdapterView<?> parent, View view, int position, 204 long i) { 205 AlertActivity alertActivity = AlertActivity.this; 206 Cursor cursor = alertActivity.getItemForView(view); 207 208 // Mark this alarm as DISMISSED 209 dismissAlarm(cursor.getLong(INDEX_ROW_ID)); 210 211 long id = cursor.getInt(AlertActivity.INDEX_EVENT_ID); 212 long startMillis = cursor.getLong(AlertActivity.INDEX_BEGIN); 213 long endMillis = cursor.getLong(AlertActivity.INDEX_END); 214 Intent eventIntent = new Intent(Intent.ACTION_VIEW); 215 Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 216 builder.appendEncodedPath("events/" + id); 217 eventIntent.setData(builder.build()); 218 eventIntent.setClass(AlertActivity.this, AllInOneActivity.class); 219 eventIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis); 220 eventIntent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis); 221 alertActivity.startActivity(eventIntent); 222 223 alertActivity.finish(); 224 } 225 }; 226 227 @Override 228 protected void onCreate(Bundle icicle) { 229 super.onCreate(icicle); 230 231 setContentView(R.layout.alert_activity); 232 setTitle(R.string.alert_title); 233 234 mQueryHandler = new QueryHandler(this); 235 mAdapter = new AlertAdapter(this, R.layout.alert_item); 236 237 mListView = (ListView) findViewById(R.id.alert_container); 238 mListView.setItemsCanFocus(true); 239 mListView.setAdapter(mAdapter); 240 mListView.setOnItemClickListener(mViewListener); 241 242 mSnoozeAllButton = (Button) findViewById(R.id.snooze_all); 243 mSnoozeAllButton.setOnClickListener(this); 244 mDismissAllButton = (Button) findViewById(R.id.dismiss_all); 245 mDismissAllButton.setOnClickListener(this); 246 247 // Disable the buttons, since they need mCursor, which is created asynchronously 248 mSnoozeAllButton.setEnabled(false); 249 mDismissAllButton.setEnabled(false); 250 } 251 252 @Override 253 protected void onResume() { 254 super.onResume(); 255 256 // If the cursor is null, start the async handler. If it is not null just requery. 257 if (mCursor == null) { 258 Uri uri = CalendarAlerts.CONTENT_URI_BY_INSTANCE; 259 mQueryHandler.startQuery(0, null, uri, PROJECTION, SELECTION, 260 SELECTIONARG, CalendarAlerts.DEFAULT_SORT_ORDER); 261 } else { 262 if (!mCursor.requery()) { 263 Log.w(TAG, "Cursor#requery() failed."); 264 mCursor.close(); 265 mCursor = null; 266 } 267 } 268 } 269 270 @Override 271 protected void onStop() { 272 super.onStop(); 273 AlertService.updateAlertNotification(this); 274 275 if (mCursor != null) { 276 mCursor.deactivate(); 277 } 278 } 279 280 @Override 281 protected void onDestroy() { 282 super.onDestroy(); 283 if (mCursor != null) { 284 mCursor.close(); 285 } 286 } 287 288 @Override 289 public void onClick(View v) { 290 if (v == mSnoozeAllButton) { 291 long alarmTime = System.currentTimeMillis() + SNOOZE_DELAY; 292 293 NotificationManager nm = 294 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 295 nm.cancel(NOTIFICATION_ID); 296 297 if (mCursor != null) { 298 long scheduleAlarmTime = 0; 299 mCursor.moveToPosition(-1); 300 while (mCursor.moveToNext()) { 301 long eventId = mCursor.getLong(INDEX_EVENT_ID); 302 long begin = mCursor.getLong(INDEX_BEGIN); 303 long end = mCursor.getLong(INDEX_END); 304 305 // Set the "minutes" to zero to indicate this is a snoozed 306 // alarm. There is code in AlertService.java that checks 307 // this field. 308 ContentValues values = 309 makeContentValues(eventId, begin, end, alarmTime, 0 /* minutes */); 310 311 // Create a new alarm entry in the CalendarAlerts table 312 if (mCursor.isLast()) { 313 scheduleAlarmTime = alarmTime; 314 } 315 mQueryHandler.startInsert(0, 316 scheduleAlarmTime, CalendarAlerts.CONTENT_URI, values, 317 Utils.UNDO_DELAY); 318 } 319 } else { 320 Log.d(TAG, "Cursor object is null. Ignore the Snooze request."); 321 } 322 323 dismissFiredAlarms(); 324 finish(); 325 } else if (v == mDismissAllButton) { 326 NotificationManager nm = 327 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 328 nm.cancel(NOTIFICATION_ID); 329 330 dismissFiredAlarms(); 331 332 finish(); 333 } 334 } 335 336 public boolean isEmpty() { 337 return mCursor != null ? (mCursor.getCount() == 0) : true; 338 } 339 340 public Cursor getItemForView(View view) { 341 final int index = mListView.getPositionForView(view); 342 if (index < 0) { 343 return null; 344 } 345 return (Cursor) mListView.getAdapter().getItem(index); 346 } 347 } 348