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