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
17package com.android.calendar.alerts;
18
19import com.android.calendar.AllInOneActivity;
20import com.android.calendar.AsyncQueryService;
21import com.android.calendar.R;
22import com.android.calendar.Utils;
23
24import android.app.Activity;
25import android.app.AlarmManager;
26import android.app.NotificationManager;
27import android.app.PendingIntent;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.Intent;
32import android.database.Cursor;
33import android.net.Uri;
34import android.net.Uri.Builder;
35import android.os.Bundle;
36import android.provider.CalendarContract;
37import android.provider.CalendarContract.CalendarAlerts;
38import android.util.Log;
39import android.view.View;
40import android.view.View.OnClickListener;
41import android.widget.AdapterView;
42import android.widget.AdapterView.OnItemClickListener;
43import android.widget.Button;
44import 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  */
50public 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