1 /* 2 * Copyright (C) 2013 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.deskclock.provider; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.database.Cursor; 25 import android.media.RingtoneManager; 26 import android.net.Uri; 27 import android.preference.PreferenceManager; 28 29 import com.android.deskclock.LogUtils; 30 import com.android.deskclock.R; 31 import com.android.deskclock.SettingsActivity; 32 33 import java.util.Calendar; 34 import java.util.LinkedList; 35 import java.util.List; 36 37 public final class AlarmInstance implements ClockContract.InstancesColumns { 38 /** 39 * Offset from alarm time to show low priority notification 40 */ 41 public static final int LOW_NOTIFICATION_HOUR_OFFSET = -2; 42 43 /** 44 * Offset from alarm time to show high priority notification 45 */ 46 public static final int HIGH_NOTIFICATION_MINUTE_OFFSET = -30; 47 48 /** 49 * Offset from alarm time to stop showing missed notification. 50 */ 51 private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12; 52 53 /** 54 * Default timeout for alarms in minutes. 55 */ 56 private static final String DEFAULT_ALARM_TIMEOUT_SETTING = "10"; 57 58 /** 59 * AlarmInstances start with an invalid id when it hasn't been saved to the database. 60 */ 61 public static final long INVALID_ID = -1; 62 63 private static final String[] QUERY_COLUMNS = { 64 _ID, 65 YEAR, 66 MONTH, 67 DAY, 68 HOUR, 69 MINUTES, 70 LABEL, 71 VIBRATE, 72 RINGTONE, 73 ALARM_ID, 74 ALARM_STATE 75 }; 76 77 /** 78 * These save calls to cursor.getColumnIndexOrThrow() 79 * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS 80 */ 81 private static final int ID_INDEX = 0; 82 private static final int YEAR_INDEX = 1; 83 private static final int MONTH_INDEX = 2; 84 private static final int DAY_INDEX = 3; 85 private static final int HOUR_INDEX = 4; 86 private static final int MINUTES_INDEX = 5; 87 private static final int LABEL_INDEX = 6; 88 private static final int VIBRATE_INDEX = 7; 89 private static final int RINGTONE_INDEX = 8; 90 private static final int ALARM_ID_INDEX = 9; 91 private static final int ALARM_STATE_INDEX = 10; 92 93 private static final int COLUMN_COUNT = ALARM_STATE_INDEX + 1; 94 createContentValues(AlarmInstance instance)95 public static ContentValues createContentValues(AlarmInstance instance) { 96 ContentValues values = new ContentValues(COLUMN_COUNT); 97 if (instance.mId != INVALID_ID) { 98 values.put(_ID, instance.mId); 99 } 100 101 values.put(YEAR, instance.mYear); 102 values.put(MONTH, instance.mMonth); 103 values.put(DAY, instance.mDay); 104 values.put(HOUR, instance.mHour); 105 values.put(MINUTES, instance.mMinute); 106 values.put(LABEL, instance.mLabel); 107 values.put(VIBRATE, instance.mVibrate ? 1 : 0); 108 if (instance.mRingtone == null) { 109 // We want to put null in the database, so we'll be able 110 // to pick up on changes to the default alarm 111 values.putNull(RINGTONE); 112 } else { 113 values.put(RINGTONE, instance.mRingtone.toString()); 114 } 115 values.put(ALARM_ID, instance.mAlarmId); 116 values.put(ALARM_STATE, instance.mAlarmState); 117 return values; 118 } 119 createIntent(String action, long instanceId)120 public static Intent createIntent(String action, long instanceId) { 121 return new Intent(action).setData(getUri(instanceId)); 122 } 123 createIntent(Context context, Class<?> cls, long instanceId)124 public static Intent createIntent(Context context, Class<?> cls, long instanceId) { 125 return new Intent(context, cls).setData(getUri(instanceId)); 126 } 127 getId(Uri contentUri)128 public static long getId(Uri contentUri) { 129 return ContentUris.parseId(contentUri); 130 } 131 getUri(long instanceId)132 public static Uri getUri(long instanceId) { 133 return ContentUris.withAppendedId(CONTENT_URI, instanceId); 134 } 135 136 /** 137 * Get alarm instance from instanceId. 138 * 139 * @param contentResolver to perform the query on. 140 * @param instanceId for the desired instance. 141 * @return instance if found, null otherwise 142 */ getInstance(ContentResolver contentResolver, long instanceId)143 public static AlarmInstance getInstance(ContentResolver contentResolver, long instanceId) { 144 Cursor cursor = contentResolver.query(getUri(instanceId), QUERY_COLUMNS, null, null, null); 145 AlarmInstance result = null; 146 if (cursor == null) { 147 return result; 148 } 149 150 try { 151 if (cursor.moveToFirst()) { 152 result = new AlarmInstance(cursor); 153 } 154 } finally { 155 cursor.close(); 156 } 157 158 return result; 159 } 160 161 /** 162 * Get an alarm instances by alarmId. 163 * 164 * @param contentResolver to perform the query on. 165 * @param alarmId of instances desired. 166 * @return list of alarms instances that are owned by alarmId. 167 */ getInstancesByAlarmId(ContentResolver contentResolver, long alarmId)168 public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver, 169 long alarmId) { 170 return getInstances(contentResolver, ALARM_ID + "=" + alarmId); 171 } 172 173 /** 174 * Get the next instance of an alarm given its alarmId 175 * @param contentResolver to perform query on 176 * @param alarmId of instance desired 177 * @return the next instance of an alarm by alarmId. 178 */ getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver, long alarmId)179 public static AlarmInstance getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver, 180 long alarmId) { 181 final List<AlarmInstance> alarmInstances = getInstancesByAlarmId(contentResolver, alarmId); 182 if (alarmInstances.isEmpty()) { 183 return null; 184 } 185 AlarmInstance nextAlarmInstance = alarmInstances.get(0); 186 for (AlarmInstance instance : alarmInstances) { 187 if (instance.getAlarmTime().before(nextAlarmInstance.getAlarmTime())) { 188 nextAlarmInstance = instance; 189 } 190 } 191 return nextAlarmInstance; 192 } 193 194 /** 195 * Get alarm instance by id and state. 196 */ getInstancesByInstanceIdAndState( ContentResolver contentResolver, long alarmInstanceId, int state)197 public static List<AlarmInstance> getInstancesByInstanceIdAndState( 198 ContentResolver contentResolver, long alarmInstanceId, int state) { 199 return getInstances(contentResolver, _ID + "=" + alarmInstanceId + " AND " + ALARM_STATE + 200 "=" + state); 201 } 202 203 /** 204 * Get alarm instances in the specified state. 205 */ getInstancesByState( ContentResolver contentResolver, int state)206 public static List<AlarmInstance> getInstancesByState( 207 ContentResolver contentResolver, int state) { 208 return getInstances(contentResolver, ALARM_STATE + "=" + state); 209 } 210 211 /** 212 * Get a list of instances given selection. 213 * 214 * @param contentResolver to perform the query on. 215 * @param selection A filter declaring which rows to return, formatted as an 216 * SQL WHERE clause (excluding the WHERE itself). Passing null will 217 * return all rows for the given URI. 218 * @param selectionArgs You may include ?s in selection, which will be 219 * replaced by the values from selectionArgs, in the order that they 220 * appear in the selection. The values will be bound as Strings. 221 * @return list of alarms matching where clause or empty list if none found. 222 */ getInstances(ContentResolver contentResolver, String selection, String ... selectionArgs)223 public static List<AlarmInstance> getInstances(ContentResolver contentResolver, 224 String selection, String ... selectionArgs) { 225 Cursor cursor = contentResolver.query(CONTENT_URI, QUERY_COLUMNS, 226 selection, selectionArgs, null); 227 List<AlarmInstance> result = new LinkedList<>(); 228 if (cursor == null) { 229 return result; 230 } 231 232 try { 233 if (cursor.moveToFirst()) { 234 do { 235 result.add(new AlarmInstance(cursor)); 236 } while (cursor.moveToNext()); 237 } 238 } finally { 239 cursor.close(); 240 } 241 242 return result; 243 } 244 addInstance(ContentResolver contentResolver, AlarmInstance instance)245 public static AlarmInstance addInstance(ContentResolver contentResolver, 246 AlarmInstance instance) { 247 // Make sure we are not adding a duplicate instances. This is not a 248 // fix and should never happen. This is only a safe guard against bad code, and you 249 // should fix the root issue if you see the error message. 250 String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId; 251 for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) { 252 if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) { 253 LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to " 254 + instance); 255 // Copy over the new instance values and update the db 256 instance.mId = otherInstances.mId; 257 updateInstance(contentResolver, instance); 258 return instance; 259 } 260 } 261 262 ContentValues values = createContentValues(instance); 263 Uri uri = contentResolver.insert(CONTENT_URI, values); 264 instance.mId = getId(uri); 265 return instance; 266 } 267 updateInstance(ContentResolver contentResolver, AlarmInstance instance)268 public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) { 269 if (instance.mId == INVALID_ID) return false; 270 ContentValues values = createContentValues(instance); 271 long rowsUpdated = contentResolver.update(getUri(instance.mId), values, null, null); 272 return rowsUpdated == 1; 273 } 274 deleteInstance(ContentResolver contentResolver, long instanceId)275 public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) { 276 if (instanceId == INVALID_ID) return false; 277 int deletedRows = contentResolver.delete(getUri(instanceId), "", null); 278 return deletedRows == 1; 279 } 280 281 /** 282 * @param contentResolver to access the content provider 283 * @param alarmId identifies the alarm in question 284 * @param instanceId identifies the instance to keep; all other instances will be removed 285 */ deleteOtherInstances(ContentResolver contentResolver, long alarmId, long instanceId)286 public static void deleteOtherInstances(ContentResolver contentResolver, long alarmId, 287 long instanceId) { 288 final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId); 289 for (AlarmInstance instance : instances) { 290 if (instance.mId != instanceId) { 291 deleteInstance(contentResolver, instance.mId); 292 } 293 } 294 } 295 296 // Public fields 297 public long mId; 298 public int mYear; 299 public int mMonth; 300 public int mDay; 301 public int mHour; 302 public int mMinute; 303 public String mLabel; 304 public boolean mVibrate; 305 public Uri mRingtone; 306 public Long mAlarmId; 307 public int mAlarmState; 308 AlarmInstance(Calendar calendar, Long alarmId)309 public AlarmInstance(Calendar calendar, Long alarmId) { 310 this(calendar); 311 mAlarmId = alarmId; 312 } 313 AlarmInstance(Calendar calendar)314 public AlarmInstance(Calendar calendar) { 315 mId = INVALID_ID; 316 setAlarmTime(calendar); 317 mLabel = ""; 318 mVibrate = false; 319 mRingtone = null; 320 mAlarmState = SILENT_STATE; 321 } 322 AlarmInstance(Cursor c)323 public AlarmInstance(Cursor c) { 324 mId = c.getLong(ID_INDEX); 325 mYear = c.getInt(YEAR_INDEX); 326 mMonth = c.getInt(MONTH_INDEX); 327 mDay = c.getInt(DAY_INDEX); 328 mHour = c.getInt(HOUR_INDEX); 329 mMinute = c.getInt(MINUTES_INDEX); 330 mLabel = c.getString(LABEL_INDEX); 331 mVibrate = c.getInt(VIBRATE_INDEX) == 1; 332 if (c.isNull(RINGTONE_INDEX)) { 333 // Should we be saving this with the current ringtone or leave it null 334 // so it changes when user changes default ringtone? 335 mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 336 } else { 337 mRingtone = Uri.parse(c.getString(RINGTONE_INDEX)); 338 } 339 340 if (!c.isNull(ALARM_ID_INDEX)) { 341 mAlarmId = c.getLong(ALARM_ID_INDEX); 342 } 343 mAlarmState = c.getInt(ALARM_STATE_INDEX); 344 } 345 getLabelOrDefault(Context context)346 public String getLabelOrDefault(Context context) { 347 return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel; 348 } 349 setAlarmTime(Calendar calendar)350 public void setAlarmTime(Calendar calendar) { 351 mYear = calendar.get(Calendar.YEAR); 352 mMonth = calendar.get(Calendar.MONTH); 353 mDay = calendar.get(Calendar.DAY_OF_MONTH); 354 mHour = calendar.get(Calendar.HOUR_OF_DAY); 355 mMinute = calendar.get(Calendar.MINUTE); 356 } 357 358 /** 359 * Return the time when a alarm should fire. 360 * 361 * @return the time 362 */ getAlarmTime()363 public Calendar getAlarmTime() { 364 Calendar calendar = Calendar.getInstance(); 365 calendar.set(Calendar.YEAR, mYear); 366 calendar.set(Calendar.MONTH, mMonth); 367 calendar.set(Calendar.DAY_OF_MONTH, mDay); 368 calendar.set(Calendar.HOUR_OF_DAY, mHour); 369 calendar.set(Calendar.MINUTE, mMinute); 370 calendar.set(Calendar.SECOND, 0); 371 calendar.set(Calendar.MILLISECOND, 0); 372 return calendar; 373 } 374 375 /** 376 * Return the time when a low priority notification should be shown. 377 * 378 * @return the time 379 */ getLowNotificationTime()380 public Calendar getLowNotificationTime() { 381 Calendar calendar = getAlarmTime(); 382 calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET); 383 return calendar; 384 } 385 386 /** 387 * Return the time when a high priority notification should be shown. 388 * 389 * @return the time 390 */ getHighNotificationTime()391 public Calendar getHighNotificationTime() { 392 Calendar calendar = getAlarmTime(); 393 calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET); 394 return calendar; 395 } 396 397 /** 398 * Return the time when a missed notification should be removed. 399 * 400 * @return the time 401 */ getMissedTimeToLive()402 public Calendar getMissedTimeToLive() { 403 Calendar calendar = getAlarmTime(); 404 calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET); 405 return calendar; 406 } 407 408 /** 409 * Return the time when the alarm should stop firing and be marked as missed. 410 * 411 * @param context to figure out the timeout setting 412 * @return the time when alarm should be silence, or null if never 413 */ getTimeout(Context context)414 public Calendar getTimeout(Context context) { 415 String timeoutSetting = PreferenceManager.getDefaultSharedPreferences(context) 416 .getString(SettingsActivity.KEY_AUTO_SILENCE, DEFAULT_ALARM_TIMEOUT_SETTING); 417 int timeoutMinutes = Integer.parseInt(timeoutSetting); 418 419 // Alarm silence has been set to "None" 420 if (timeoutMinutes < 0) { 421 return null; 422 } 423 424 Calendar calendar = getAlarmTime(); 425 calendar.add(Calendar.MINUTE, timeoutMinutes); 426 return calendar; 427 } 428 429 @Override equals(Object o)430 public boolean equals(Object o) { 431 if (!(o instanceof AlarmInstance)) return false; 432 final AlarmInstance other = (AlarmInstance) o; 433 return mId == other.mId; 434 } 435 436 @Override hashCode()437 public int hashCode() { 438 return Long.valueOf(mId).hashCode(); 439 } 440 441 @Override toString()442 public String toString() { 443 return "AlarmInstance{" + 444 "mId=" + mId + 445 ", mYear=" + mYear + 446 ", mMonth=" + mMonth + 447 ", mDay=" + mDay + 448 ", mHour=" + mHour + 449 ", mMinute=" + mMinute + 450 ", mLabel=" + mLabel + 451 ", mVibrate=" + mVibrate + 452 ", mRingtone=" + mRingtone + 453 ", mAlarmId=" + mAlarmId + 454 ", mAlarmState=" + mAlarmState + 455 '}'; 456 } 457 } 458