AlarmInstance.java revision 595e6b8c56a6810d0d3bd09669ae314fe0d5a902
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; 27import android.preference.PreferenceManager; 28 29import com.android.deskclock.LogUtils; 30import com.android.deskclock.R; 31import com.android.deskclock.settings.SettingsActivity; 32 33import java.util.Calendar; 34import java.util.LinkedList; 35import java.util.List; 36 37public 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 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 120 public static Intent createIntent(String action, long instanceId) { 121 return new Intent(action).setData(getUri(instanceId)); 122 } 123 124 public static Intent createIntent(Context context, Class<?> cls, long instanceId) { 125 return new Intent(context, cls).setData(getUri(instanceId)); 126 } 127 128 public static long getId(Uri contentUri) { 129 return ContentUris.parseId(contentUri); 130 } 131 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 cr to perform the query on. 140 * @param instanceId for the desired instance. 141 * @return instance if found, null otherwise 142 */ 143 public static AlarmInstance getInstance(ContentResolver cr, long instanceId) { 144 try (Cursor cursor = cr.query(getUri(instanceId), QUERY_COLUMNS, null, null, null)) { 145 if (cursor != null && cursor.moveToFirst()) { 146 return new AlarmInstance(cursor, false /* joinedTable */); 147 } 148 } 149 150 return null; 151 } 152 153 /** 154 * Get an alarm instances by alarmId. 155 * 156 * @param contentResolver to perform the query on. 157 * @param alarmId of instances desired. 158 * @return list of alarms instances that are owned by alarmId. 159 */ 160 public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver, 161 long alarmId) { 162 return getInstances(contentResolver, ALARM_ID + "=" + alarmId); 163 } 164 165 /** 166 * Get the next instance of an alarm given its alarmId 167 * @param contentResolver to perform query on 168 * @param alarmId of instance desired 169 * @return the next instance of an alarm by alarmId. 170 */ 171 public static AlarmInstance getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver, 172 long alarmId) { 173 final List<AlarmInstance> alarmInstances = getInstancesByAlarmId(contentResolver, alarmId); 174 if (alarmInstances.isEmpty()) { 175 return null; 176 } 177 AlarmInstance nextAlarmInstance = alarmInstances.get(0); 178 for (AlarmInstance instance : alarmInstances) { 179 if (instance.getAlarmTime().before(nextAlarmInstance.getAlarmTime())) { 180 nextAlarmInstance = instance; 181 } 182 } 183 return nextAlarmInstance; 184 } 185 186 /** 187 * Get alarm instance by id and state. 188 */ 189 public static List<AlarmInstance> getInstancesByInstanceIdAndState( 190 ContentResolver contentResolver, long alarmInstanceId, int state) { 191 return getInstances(contentResolver, _ID + "=" + alarmInstanceId + " AND " + ALARM_STATE + 192 "=" + state); 193 } 194 195 /** 196 * Get alarm instances in the specified state. 197 */ 198 public static List<AlarmInstance> getInstancesByState( 199 ContentResolver contentResolver, int state) { 200 return getInstances(contentResolver, ALARM_STATE + "=" + state); 201 } 202 203 /** 204 * Get a list of instances given selection. 205 * 206 * @param cr to perform the query on. 207 * @param selection A filter declaring which rows to return, formatted as an 208 * SQL WHERE clause (excluding the WHERE itself). Passing null will 209 * return all rows for the given URI. 210 * @param selectionArgs You may include ?s in selection, which will be 211 * replaced by the values from selectionArgs, in the order that they 212 * appear in the selection. The values will be bound as Strings. 213 * @return list of alarms matching where clause or empty list if none found. 214 */ 215 public static List<AlarmInstance> getInstances(ContentResolver cr, String selection, 216 String... selectionArgs) { 217 final List<AlarmInstance> result = new LinkedList<>(); 218 try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) { 219 if (cursor.moveToFirst()) { 220 do { 221 result.add(new AlarmInstance(cursor, false /* joinedTable */)); 222 } while (cursor.moveToNext()); 223 } 224 } 225 226 return result; 227 } 228 229 public static AlarmInstance addInstance(ContentResolver contentResolver, 230 AlarmInstance instance) { 231 // Make sure we are not adding a duplicate instances. This is not a 232 // fix and should never happen. This is only a safe guard against bad code, and you 233 // should fix the root issue if you see the error message. 234 String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId; 235 for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) { 236 if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) { 237 LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to " 238 + instance); 239 // Copy over the new instance values and update the db 240 instance.mId = otherInstances.mId; 241 updateInstance(contentResolver, instance); 242 return instance; 243 } 244 } 245 246 ContentValues values = createContentValues(instance); 247 Uri uri = contentResolver.insert(CONTENT_URI, values); 248 instance.mId = getId(uri); 249 return instance; 250 } 251 252 public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) { 253 if (instance.mId == INVALID_ID) return false; 254 ContentValues values = createContentValues(instance); 255 long rowsUpdated = contentResolver.update(getUri(instance.mId), values, null, null); 256 return rowsUpdated == 1; 257 } 258 259 public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) { 260 if (instanceId == INVALID_ID) return false; 261 int deletedRows = contentResolver.delete(getUri(instanceId), "", null); 262 return deletedRows == 1; 263 } 264 265 /** 266 * @param contentResolver to access the content provider 267 * @param alarmId identifies the alarm in question 268 * @param instanceId identifies the instance to keep; all other instances will be removed 269 */ 270 public static void deleteOtherInstances(ContentResolver contentResolver, long alarmId, 271 long instanceId) { 272 final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId); 273 for (AlarmInstance instance : instances) { 274 if (instance.mId != instanceId) { 275 deleteInstance(contentResolver, instance.mId); 276 } 277 } 278 } 279 280 // Public fields 281 public long mId; 282 public int mYear; 283 public int mMonth; 284 public int mDay; 285 public int mHour; 286 public int mMinute; 287 public String mLabel; 288 public boolean mVibrate; 289 public Uri mRingtone; 290 public Long mAlarmId; 291 public int mAlarmState; 292 293 public AlarmInstance(Calendar calendar, Long alarmId) { 294 this(calendar); 295 mAlarmId = alarmId; 296 } 297 298 public AlarmInstance(Calendar calendar) { 299 mId = INVALID_ID; 300 setAlarmTime(calendar); 301 mLabel = ""; 302 mVibrate = false; 303 mRingtone = null; 304 mAlarmState = SILENT_STATE; 305 } 306 307 public AlarmInstance(AlarmInstance instance) { 308 this.mId = instance.mId; 309 this.mYear = instance.mYear; 310 this.mMonth = instance.mMonth; 311 this.mDay = instance.mDay; 312 this.mHour = instance.mHour; 313 this.mMinute = instance.mMinute; 314 this.mLabel = instance.mLabel; 315 this.mVibrate = instance.mVibrate; 316 this.mRingtone = instance.mRingtone; 317 this.mAlarmId = instance.mAlarmId; 318 this.mAlarmState = instance.mAlarmState; 319 } 320 321 public AlarmInstance(Cursor c, boolean joinedTable) { 322 if (joinedTable) { 323 mId = c.getLong(Alarm.INSTANCE_ID_INDEX); 324 mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX); 325 mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX); 326 mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX); 327 mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX); 328 mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX); 329 mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX); 330 mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1; 331 } else { 332 mId = c.getLong(ID_INDEX); 333 mYear = c.getInt(YEAR_INDEX); 334 mMonth = c.getInt(MONTH_INDEX); 335 mDay = c.getInt(DAY_INDEX); 336 mHour = c.getInt(HOUR_INDEX); 337 mMinute = c.getInt(MINUTES_INDEX); 338 mLabel = c.getString(LABEL_INDEX); 339 mVibrate = c.getInt(VIBRATE_INDEX) == 1; 340 } 341 if (c.isNull(RINGTONE_INDEX)) { 342 // Should we be saving this with the current ringtone or leave it null 343 // so it changes when user changes default ringtone? 344 mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 345 } else { 346 mRingtone = Uri.parse(c.getString(RINGTONE_INDEX)); 347 } 348 349 if (!c.isNull(ALARM_ID_INDEX)) { 350 mAlarmId = c.getLong(ALARM_ID_INDEX); 351 } 352 mAlarmState = c.getInt(ALARM_STATE_INDEX); 353 } 354 355 public String getLabelOrDefault(Context context) { 356 return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel; 357 } 358 359 public void setAlarmTime(Calendar calendar) { 360 mYear = calendar.get(Calendar.YEAR); 361 mMonth = calendar.get(Calendar.MONTH); 362 mDay = calendar.get(Calendar.DAY_OF_MONTH); 363 mHour = calendar.get(Calendar.HOUR_OF_DAY); 364 mMinute = calendar.get(Calendar.MINUTE); 365 } 366 367 /** 368 * Return the time when a alarm should fire. 369 * 370 * @return the time 371 */ 372 public Calendar getAlarmTime() { 373 Calendar calendar = Calendar.getInstance(); 374 calendar.set(Calendar.YEAR, mYear); 375 calendar.set(Calendar.MONTH, mMonth); 376 calendar.set(Calendar.DAY_OF_MONTH, mDay); 377 calendar.set(Calendar.HOUR_OF_DAY, mHour); 378 calendar.set(Calendar.MINUTE, mMinute); 379 calendar.set(Calendar.SECOND, 0); 380 calendar.set(Calendar.MILLISECOND, 0); 381 return calendar; 382 } 383 384 /** 385 * Return the time when a low priority notification should be shown. 386 * 387 * @return the time 388 */ 389 public Calendar getLowNotificationTime() { 390 Calendar calendar = getAlarmTime(); 391 calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET); 392 return calendar; 393 } 394 395 /** 396 * Return the time when a high priority notification should be shown. 397 * 398 * @return the time 399 */ 400 public Calendar getHighNotificationTime() { 401 Calendar calendar = getAlarmTime(); 402 calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET); 403 return calendar; 404 } 405 406 /** 407 * Return the time when a missed notification should be removed. 408 * 409 * @return the time 410 */ 411 public Calendar getMissedTimeToLive() { 412 Calendar calendar = getAlarmTime(); 413 calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET); 414 return calendar; 415 } 416 417 /** 418 * Return the time when the alarm should stop firing and be marked as missed. 419 * 420 * @param context to figure out the timeout setting 421 * @return the time when alarm should be silence, or null if never 422 */ 423 public Calendar getTimeout(Context context) { 424 String timeoutSetting = PreferenceManager.getDefaultSharedPreferences(context) 425 .getString(SettingsActivity.KEY_AUTO_SILENCE, DEFAULT_ALARM_TIMEOUT_SETTING); 426 int timeoutMinutes = Integer.parseInt(timeoutSetting); 427 428 // Alarm silence has been set to "None" 429 if (timeoutMinutes < 0) { 430 return null; 431 } 432 433 Calendar calendar = getAlarmTime(); 434 calendar.add(Calendar.MINUTE, timeoutMinutes); 435 return calendar; 436 } 437 438 @Override 439 public boolean equals(Object o) { 440 if (!(o instanceof AlarmInstance)) return false; 441 final AlarmInstance other = (AlarmInstance) o; 442 return mId == other.mId; 443 } 444 445 @Override 446 public int hashCode() { 447 return Long.valueOf(mId).hashCode(); 448 } 449 450 @Override 451 public String toString() { 452 return "AlarmInstance{" + 453 "mId=" + mId + 454 ", mYear=" + mYear + 455 ", mMonth=" + mMonth + 456 ", mDay=" + mDay + 457 ", mHour=" + mHour + 458 ", mMinute=" + mMinute + 459 ", mLabel=" + mLabel + 460 ", mVibrate=" + mVibrate + 461 ", mRingtone=" + mRingtone + 462 ", mAlarmId=" + mAlarmId + 463 ", mAlarmState=" + mAlarmState + 464 '}'; 465 } 466} 467