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