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