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