Home | History | Annotate | Download | only in alerts
      1 /*
      2  * Copyright (C) 2012 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 android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.SharedPreferences;
     26 import android.net.Uri;
     27 import android.provider.CalendarContract;
     28 import android.provider.CalendarContract.CalendarAlerts;
     29 import android.text.TextUtils;
     30 import android.text.format.DateFormat;
     31 import android.text.format.DateUtils;
     32 import android.text.format.Time;
     33 import android.util.Log;
     34 
     35 import com.android.calendar.EventInfoActivity;
     36 import com.android.calendar.R;
     37 import com.android.calendar.Utils;
     38 
     39 import java.util.Locale;
     40 import java.util.Map;
     41 import java.util.TimeZone;
     42 
     43 public class AlertUtils {
     44     private static final String TAG = "AlertUtils";
     45     static final boolean DEBUG = true;
     46 
     47     public static final long SNOOZE_DELAY = 5 * 60 * 1000L;
     48 
     49     // We use one notification id for the expired events notification.  All
     50     // other notifications (the 'active' future/concurrent ones) use a unique ID.
     51     public static final int EXPIRED_GROUP_NOTIFICATION_ID = 0;
     52 
     53     public static final String EVENT_ID_KEY = "eventid";
     54     public static final String EVENT_START_KEY = "eventstart";
     55     public static final String EVENT_END_KEY = "eventend";
     56     public static final String NOTIFICATION_ID_KEY = "notificationid";
     57     public static final String EVENT_IDS_KEY = "eventids";
     58     public static final String EVENT_STARTS_KEY = "starts";
     59 
     60     // A flag for using local storage to save alert state instead of the alerts DB table.
     61     // This allows the unbundled app to run alongside other calendar apps without eating
     62     // alerts from other apps.
     63     static boolean BYPASS_DB = true;
     64 
     65     // SharedPrefs table name for storing fired alerts.  This prevents other installed
     66     // Calendar apps from eating the alerts.
     67     private static final String ALERTS_SHARED_PREFS_NAME = "calendar_alerts";
     68 
     69     // Keyname prefix for the alerts data in SharedPrefs.  The key will contain a combo
     70     // of event ID, begin time, and alarm time.  The value will be the fired time.
     71     private static final String KEY_FIRED_ALERT_PREFIX = "preference_alert_";
     72 
     73     // The last time the SharedPrefs was scanned and flushed of old alerts data.
     74     private static final String KEY_LAST_FLUSH_TIME_MS = "preference_flushTimeMs";
     75 
     76     // The # of days to save alert states in the shared prefs table, before flushing.  This
     77     // can be any value, since AlertService will also check for a recent alertTime before
     78     // ringing the alert.
     79     private static final int FLUSH_INTERVAL_DAYS = 1;
     80     private static final int FLUSH_INTERVAL_MS = FLUSH_INTERVAL_DAYS * 24 * 60 * 60 * 1000;
     81 
     82     /**
     83      * Creates an AlarmManagerInterface that wraps a real AlarmManager.  The alarm code
     84      * was abstracted to an interface to make it testable.
     85      */
     86     public static AlarmManagerInterface createAlarmManager(Context context) {
     87         final AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
     88         return new AlarmManagerInterface() {
     89             @Override
     90             public void set(int type, long triggerAtMillis, PendingIntent operation) {
     91                 if (Utils.isKeyLimePieOrLater()) {
     92                     mgr.setExact(type, triggerAtMillis, operation);
     93                 } else {
     94                     mgr.set(type, triggerAtMillis, operation);
     95                 }
     96             }
     97         };
     98     }
     99 
    100     /**
    101      * Schedules an alarm intent with the system AlarmManager that will notify
    102      * listeners when a reminder should be fired. The provider will keep
    103      * scheduled reminders up to date but apps may use this to implement snooze
    104      * functionality without modifying the reminders table. Scheduled alarms
    105      * will generate an intent using AlertReceiver.EVENT_REMINDER_APP_ACTION.
    106      *
    107      * @param context A context for referencing system resources
    108      * @param manager The AlarmManager to use or null
    109      * @param alarmTime The time to fire the intent in UTC millis since epoch
    110      */
    111     public static void scheduleAlarm(Context context, AlarmManagerInterface manager,
    112             long alarmTime) {
    113         scheduleAlarmHelper(context, manager, alarmTime, false);
    114     }
    115 
    116     /**
    117      * Schedules the next alarm to silently refresh the notifications.  Note that if there
    118      * is a pending silent refresh alarm, it will be replaced with this one.
    119      */
    120     static void scheduleNextNotificationRefresh(Context context, AlarmManagerInterface manager,
    121             long alarmTime) {
    122         scheduleAlarmHelper(context, manager, alarmTime, true);
    123     }
    124 
    125     private static void scheduleAlarmHelper(Context context, AlarmManagerInterface manager,
    126             long alarmTime, boolean quietUpdate) {
    127         int alarmType = AlarmManager.RTC_WAKEUP;
    128         Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION);
    129         intent.setClass(context, AlertReceiver.class);
    130         if (quietUpdate) {
    131             alarmType = AlarmManager.RTC;
    132         } else {
    133             // Set data field so we get a unique PendingIntent instance per alarm or else alarms
    134             // may be dropped.
    135             Uri.Builder builder = CalendarAlerts.CONTENT_URI.buildUpon();
    136             ContentUris.appendId(builder, alarmTime);
    137             intent.setData(builder.build());
    138         }
    139 
    140         intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
    141         PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent,
    142                 PendingIntent.FLAG_UPDATE_CURRENT);
    143         manager.set(alarmType, alarmTime, pi);
    144     }
    145 
    146     /**
    147      * Format the second line which shows time and location for single alert or the
    148      * number of events for multiple alerts
    149      *     1) Show time only for non-all day events
    150      *     2) No date for today
    151      *     3) Show "tomorrow" for tomorrow
    152      *     4) Show date for days beyond that
    153      */
    154     static String formatTimeLocation(Context context, long startMillis, boolean allDay,
    155             String location) {
    156         String tz = Utils.getTimeZone(context, null);
    157         Time time = new Time(tz);
    158         time.setToNow();
    159         int today = Time.getJulianDay(time.toMillis(false), time.gmtoff);
    160         time.set(startMillis);
    161         int eventDay = Time.getJulianDay(time.toMillis(false), allDay ? 0 : time.gmtoff);
    162 
    163         int flags = DateUtils.FORMAT_ABBREV_ALL;
    164         if (!allDay) {
    165             flags |= DateUtils.FORMAT_SHOW_TIME;
    166             if (DateFormat.is24HourFormat(context)) {
    167                 flags |= DateUtils.FORMAT_24HOUR;
    168             }
    169         } else {
    170             flags |= DateUtils.FORMAT_UTC;
    171         }
    172 
    173         if (eventDay < today || eventDay > today + 1) {
    174             flags |= DateUtils.FORMAT_SHOW_DATE;
    175         }
    176 
    177         StringBuilder sb = new StringBuilder(Utils.formatDateRange(context, startMillis,
    178                 startMillis, flags));
    179 
    180         if (!allDay && tz != Time.getCurrentTimezone()) {
    181             // Assumes time was set to the current tz
    182             time.set(startMillis);
    183             boolean isDST = time.isDst != 0;
    184             sb.append(" ").append(TimeZone.getTimeZone(tz).getDisplayName(
    185                     isDST, TimeZone.SHORT, Locale.getDefault()));
    186         }
    187 
    188         if (eventDay == today + 1) {
    189             // Tomorrow
    190             sb.append(", ");
    191             sb.append(context.getString(R.string.tomorrow));
    192         }
    193 
    194         String loc;
    195         if (location != null && !TextUtils.isEmpty(loc = location.trim())) {
    196             sb.append(", ");
    197             sb.append(loc);
    198         }
    199         return sb.toString();
    200     }
    201 
    202     public static ContentValues makeContentValues(long eventId, long begin, long end,
    203             long alarmTime, int minutes) {
    204         ContentValues values = new ContentValues();
    205         values.put(CalendarAlerts.EVENT_ID, eventId);
    206         values.put(CalendarAlerts.BEGIN, begin);
    207         values.put(CalendarAlerts.END, end);
    208         values.put(CalendarAlerts.ALARM_TIME, alarmTime);
    209         long currentTime = System.currentTimeMillis();
    210         values.put(CalendarAlerts.CREATION_TIME, currentTime);
    211         values.put(CalendarAlerts.RECEIVED_TIME, 0);
    212         values.put(CalendarAlerts.NOTIFY_TIME, 0);
    213         values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_SCHEDULED);
    214         values.put(CalendarAlerts.MINUTES, minutes);
    215         return values;
    216     }
    217 
    218     public static Intent buildEventViewIntent(Context c, long eventId, long begin, long end) {
    219         Intent i = new Intent(Intent.ACTION_VIEW);
    220         Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
    221         builder.appendEncodedPath("events/" + eventId);
    222         i.setData(builder.build());
    223         i.setClass(c, EventInfoActivity.class);
    224         i.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, begin);
    225         i.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end);
    226         return i;
    227     }
    228 
    229     public static SharedPreferences getFiredAlertsTable(Context context) {
    230         return context.getSharedPreferences(ALERTS_SHARED_PREFS_NAME, Context.MODE_PRIVATE);
    231     }
    232 
    233     private static String getFiredAlertsKey(long eventId, long beginTime,
    234             long alarmTime) {
    235         StringBuilder sb = new StringBuilder(KEY_FIRED_ALERT_PREFIX);
    236         sb.append(eventId);
    237         sb.append("_");
    238         sb.append(beginTime);
    239         sb.append("_");
    240         sb.append(alarmTime);
    241         return sb.toString();
    242     }
    243 
    244     /**
    245      * Returns whether the SharedPrefs storage indicates we have fired the alert before.
    246      */
    247     static boolean hasAlertFiredInSharedPrefs(Context context, long eventId, long beginTime,
    248             long alarmTime) {
    249         SharedPreferences prefs = getFiredAlertsTable(context);
    250         return prefs.contains(getFiredAlertsKey(eventId, beginTime, alarmTime));
    251     }
    252 
    253     /**
    254      * Store fired alert info in the SharedPrefs.
    255      */
    256     static void setAlertFiredInSharedPrefs(Context context, long eventId, long beginTime,
    257             long alarmTime) {
    258         // Store alarm time as the value too so we don't have to parse all the keys to flush
    259         // old alarms out of the table later.
    260         SharedPreferences prefs = getFiredAlertsTable(context);
    261         SharedPreferences.Editor editor = prefs.edit();
    262         editor.putLong(getFiredAlertsKey(eventId, beginTime, alarmTime), alarmTime);
    263         editor.apply();
    264     }
    265 
    266     /**
    267      * Scans and flushes the internal storage of old alerts.  Looks up the previous flush
    268      * time in SharedPrefs, and performs the flush if overdue.  Otherwise, no-op.
    269      */
    270     static void flushOldAlertsFromInternalStorage(Context context) {
    271         if (BYPASS_DB) {
    272             SharedPreferences prefs = getFiredAlertsTable(context);
    273 
    274             // Only flush if it hasn't been done in a while.
    275             long nowTime = System.currentTimeMillis();
    276             long lastFlushTimeMs = prefs.getLong(KEY_LAST_FLUSH_TIME_MS, 0);
    277             if (nowTime - lastFlushTimeMs > FLUSH_INTERVAL_MS) {
    278                 if (DEBUG) {
    279                     Log.d(TAG, "Flushing old alerts from shared prefs table");
    280                 }
    281 
    282                 // Scan through all fired alert entries, removing old ones.
    283                 SharedPreferences.Editor editor = prefs.edit();
    284                 Time timeObj = new Time();
    285                 for (Map.Entry<String, ?> entry : prefs.getAll().entrySet()) {
    286                     String key = entry.getKey();
    287                     Object value = entry.getValue();
    288                     if (key.startsWith(KEY_FIRED_ALERT_PREFIX)) {
    289                         long alertTime;
    290                         if (value instanceof Long) {
    291                             alertTime = (Long) value;
    292                         } else {
    293                             // Should never occur.
    294                             Log.e(TAG,"SharedPrefs key " + key + " did not have Long value: " +
    295                                     value);
    296                             continue;
    297                         }
    298 
    299                         if (nowTime - alertTime >= FLUSH_INTERVAL_MS) {
    300                             editor.remove(key);
    301                             if (DEBUG) {
    302                                 int ageInDays = getIntervalInDays(alertTime, nowTime, timeObj);
    303                                 Log.d(TAG, "SharedPrefs key " + key + ": removed (" + ageInDays +
    304                                         " days old)");
    305                             }
    306                         } else {
    307                             if (DEBUG) {
    308                                 int ageInDays = getIntervalInDays(alertTime, nowTime, timeObj);
    309                                 Log.d(TAG, "SharedPrefs key " + key + ": keep (" + ageInDays +
    310                                         " days old)");
    311                             }
    312                         }
    313                     }
    314                 }
    315                 editor.putLong(KEY_LAST_FLUSH_TIME_MS, nowTime);
    316                 editor.apply();
    317             }
    318         }
    319     }
    320 
    321     private static int getIntervalInDays(long startMillis, long endMillis, Time timeObj) {
    322         timeObj.set(startMillis);
    323         int startDay = Time.getJulianDay(startMillis, timeObj.gmtoff);
    324         timeObj.set(endMillis);
    325         return Time.getJulianDay(endMillis, timeObj.gmtoff) - startDay;
    326     }
    327 }
    328