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.CursorLoader; 24 import android.content.Intent; 25 import android.database.Cursor; 26 import android.media.RingtoneManager; 27 import android.net.Uri; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 31 import com.android.deskclock.R; 32 33 import java.util.Calendar; 34 import java.util.LinkedList; 35 import java.util.List; 36 37 public final class Alarm implements Parcelable, ClockContract.AlarmsColumns { 38 /** 39 * Alarms start with an invalid id when it hasn't been saved to the database. 40 */ 41 public static final long INVALID_ID = -1; 42 43 /** 44 * The default sort order for this table 45 */ 46 private static final String DEFAULT_SORT_ORDER = 47 HOUR + ", " + 48 MINUTES + " ASC" + ", " + 49 _ID + " DESC"; 50 51 private static final String[] QUERY_COLUMNS = { 52 _ID, 53 HOUR, 54 MINUTES, 55 DAYS_OF_WEEK, 56 ENABLED, 57 VIBRATE, 58 LABEL, 59 RINGTONE, 60 DELETE_AFTER_USE 61 }; 62 63 /** 64 * These save calls to cursor.getColumnIndexOrThrow() 65 * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS 66 */ 67 private static final int ID_INDEX = 0; 68 private static final int HOUR_INDEX = 1; 69 private static final int MINUTES_INDEX = 2; 70 private static final int DAYS_OF_WEEK_INDEX = 3; 71 private static final int ENABLED_INDEX = 4; 72 private static final int VIBRATE_INDEX = 5; 73 private static final int LABEL_INDEX = 6; 74 private static final int RINGTONE_INDEX = 7; 75 private static final int DELETE_AFTER_USE_INDEX = 8; 76 77 private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1; 78 createContentValues(Alarm alarm)79 public static ContentValues createContentValues(Alarm alarm) { 80 ContentValues values = new ContentValues(COLUMN_COUNT); 81 if (alarm.id != INVALID_ID) { 82 values.put(ClockContract.AlarmsColumns._ID, alarm.id); 83 } 84 85 values.put(ENABLED, alarm.enabled ? 1 : 0); 86 values.put(HOUR, alarm.hour); 87 values.put(MINUTES, alarm.minutes); 88 values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBitSet()); 89 values.put(VIBRATE, alarm.vibrate ? 1 : 0); 90 values.put(LABEL, alarm.label); 91 values.put(DELETE_AFTER_USE, alarm.deleteAfterUse); 92 if (alarm.alert == null) { 93 // We want to put null, so default alarm changes 94 values.putNull(RINGTONE); 95 } else { 96 values.put(RINGTONE, alarm.alert.toString()); 97 } 98 99 return values; 100 } 101 createIntent(String action, long alarmId)102 public static Intent createIntent(String action, long alarmId) { 103 return new Intent(action).setData(getUri(alarmId)); 104 } 105 createIntent(Context context, Class<?> cls, long alarmId)106 public static Intent createIntent(Context context, Class<?> cls, long alarmId) { 107 return new Intent(context, cls).setData(getUri(alarmId)); 108 } 109 getUri(long alarmId)110 public static Uri getUri(long alarmId) { 111 return ContentUris.withAppendedId(CONTENT_URI, alarmId); 112 } 113 getId(Uri contentUri)114 public static long getId(Uri contentUri) { 115 return ContentUris.parseId(contentUri); 116 } 117 118 /** 119 * Get alarm cursor loader for all alarms. 120 * 121 * @param context to query the database. 122 * @return cursor loader with all the alarms. 123 */ getAlarmsCursorLoader(Context context)124 public static CursorLoader getAlarmsCursorLoader(Context context) { 125 return new CursorLoader(context, ClockContract.AlarmsColumns.CONTENT_URI, 126 QUERY_COLUMNS, null, null, DEFAULT_SORT_ORDER); 127 } 128 129 /** 130 * Get alarm by id. 131 * 132 * @param contentResolver to perform the query on. 133 * @param alarmId for the desired alarm. 134 * @return alarm if found, null otherwise 135 */ getAlarm(ContentResolver contentResolver, long alarmId)136 public static Alarm getAlarm(ContentResolver contentResolver, long alarmId) { 137 Cursor cursor = contentResolver.query(getUri(alarmId), QUERY_COLUMNS, null, null, null); 138 Alarm result = null; 139 if (cursor == null) { 140 return result; 141 } 142 143 try { 144 if (cursor.moveToFirst()) { 145 result = new Alarm(cursor); 146 } 147 } finally { 148 cursor.close(); 149 } 150 151 return result; 152 } 153 154 /** 155 * Get all alarms given conditions. 156 * 157 * @param contentResolver to perform the query on. 158 * @param selection A filter declaring which rows to return, formatted as an 159 * SQL WHERE clause (excluding the WHERE itself). Passing null will 160 * return all rows for the given URI. 161 * @param selectionArgs You may include ?s in selection, which will be 162 * replaced by the values from selectionArgs, in the order that they 163 * appear in the selection. The values will be bound as Strings. 164 * @return list of alarms matching where clause or empty list if none found. 165 */ getAlarms(ContentResolver contentResolver, String selection, String ... selectionArgs)166 public static List<Alarm> getAlarms(ContentResolver contentResolver, 167 String selection, String ... selectionArgs) { 168 Cursor cursor = contentResolver.query(CONTENT_URI, QUERY_COLUMNS, 169 selection, selectionArgs, null); 170 List<Alarm> result = new LinkedList<Alarm>(); 171 if (cursor == null) { 172 return result; 173 } 174 175 try { 176 if (cursor.moveToFirst()) { 177 do { 178 result.add(new Alarm(cursor)); 179 } while (cursor.moveToNext()); 180 } 181 } finally { 182 cursor.close(); 183 } 184 185 return result; 186 } 187 isTomorrow(Alarm alarm)188 public static boolean isTomorrow(Alarm alarm) { 189 final Calendar now = Calendar.getInstance(); 190 final int alarmHour = alarm.hour; 191 final int currHour = now.get(Calendar.HOUR_OF_DAY); 192 return alarmHour < currHour || 193 (alarmHour == currHour && alarm.minutes <= now.get(Calendar.MINUTE)); 194 } 195 addAlarm(ContentResolver contentResolver, Alarm alarm)196 public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) { 197 ContentValues values = createContentValues(alarm); 198 Uri uri = contentResolver.insert(CONTENT_URI, values); 199 alarm.id = getId(uri); 200 return alarm; 201 } 202 updateAlarm(ContentResolver contentResolver, Alarm alarm)203 public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) { 204 if (alarm.id == Alarm.INVALID_ID) return false; 205 ContentValues values = createContentValues(alarm); 206 long rowsUpdated = contentResolver.update(getUri(alarm.id), values, null, null); 207 return rowsUpdated == 1; 208 } 209 deleteAlarm(ContentResolver contentResolver, long alarmId)210 public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) { 211 if (alarmId == INVALID_ID) return false; 212 int deletedRows = contentResolver.delete(getUri(alarmId), "", null); 213 return deletedRows == 1; 214 } 215 216 public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() { 217 public Alarm createFromParcel(Parcel p) { 218 return new Alarm(p); 219 } 220 221 public Alarm[] newArray(int size) { 222 return new Alarm[size]; 223 } 224 }; 225 226 // Public fields 227 // TODO: Refactor instance names 228 public long id; 229 public boolean enabled; 230 public int hour; 231 public int minutes; 232 public DaysOfWeek daysOfWeek; 233 public boolean vibrate; 234 public String label; 235 public Uri alert; 236 public boolean deleteAfterUse; 237 238 // Creates a default alarm at the current time. Alarm()239 public Alarm() { 240 this(0, 0); 241 } 242 Alarm(int hour, int minutes)243 public Alarm(int hour, int minutes) { 244 this.id = INVALID_ID; 245 this.hour = hour; 246 this.minutes = minutes; 247 this.vibrate = true; 248 this.daysOfWeek = new DaysOfWeek(0); 249 this.label = ""; 250 this.alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 251 this.deleteAfterUse = false; 252 } 253 Alarm(Cursor c)254 public Alarm(Cursor c) { 255 id = c.getLong(ID_INDEX); 256 enabled = c.getInt(ENABLED_INDEX) == 1; 257 hour = c.getInt(HOUR_INDEX); 258 minutes = c.getInt(MINUTES_INDEX); 259 daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX)); 260 vibrate = c.getInt(VIBRATE_INDEX) == 1; 261 label = c.getString(LABEL_INDEX); 262 deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1; 263 264 if (c.isNull(RINGTONE_INDEX)) { 265 // Should we be saving this with the current ringtone or leave it null 266 // so it changes when user changes default ringtone? 267 alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 268 } else { 269 alert = Uri.parse(c.getString(RINGTONE_INDEX)); 270 } 271 } 272 Alarm(Parcel p)273 Alarm(Parcel p) { 274 id = p.readLong(); 275 enabled = p.readInt() == 1; 276 hour = p.readInt(); 277 minutes = p.readInt(); 278 daysOfWeek = new DaysOfWeek(p.readInt()); 279 vibrate = p.readInt() == 1; 280 label = p.readString(); 281 alert = (Uri) p.readParcelable(null); 282 deleteAfterUse = p.readInt() == 1; 283 } 284 getLabelOrDefault(Context context)285 public String getLabelOrDefault(Context context) { 286 if (label == null || label.length() == 0) { 287 return context.getString(R.string.default_label); 288 } 289 return label; 290 } 291 writeToParcel(Parcel p, int flags)292 public void writeToParcel(Parcel p, int flags) { 293 p.writeLong(id); 294 p.writeInt(enabled ? 1 : 0); 295 p.writeInt(hour); 296 p.writeInt(minutes); 297 p.writeInt(daysOfWeek.getBitSet()); 298 p.writeInt(vibrate ? 1 : 0); 299 p.writeString(label); 300 p.writeParcelable(alert, flags); 301 p.writeInt(deleteAfterUse ? 1 : 0); 302 } 303 describeContents()304 public int describeContents() { 305 return 0; 306 } 307 createInstanceAfter(Calendar time)308 public AlarmInstance createInstanceAfter(Calendar time) { 309 Calendar nextInstanceTime = getNextAlarmTime(time); 310 AlarmInstance result = new AlarmInstance(nextInstanceTime, id); 311 result.mVibrate = vibrate; 312 result.mLabel = label; 313 result.mRingtone = alert; 314 return result; 315 } 316 317 /** 318 * 319 * @param currentTime 320 * @return Previous firing time, or null if this is a one-time alarm. 321 */ getPreviousAlarmTime(Calendar currentTime)322 public Calendar getPreviousAlarmTime(Calendar currentTime) { 323 Calendar previousInstanceTime = Calendar.getInstance(); 324 previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR)); 325 previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH)); 326 previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH)); 327 previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 328 previousInstanceTime.set(Calendar.MINUTE, minutes); 329 previousInstanceTime.set(Calendar.SECOND, 0); 330 previousInstanceTime.set(Calendar.MILLISECOND, 0); 331 332 int subtractDays = daysOfWeek.calculateDaysToPreviousAlarm(previousInstanceTime); 333 if (subtractDays > 0) { 334 previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays); 335 return previousInstanceTime; 336 } else { 337 return null; 338 } 339 } 340 getNextAlarmTime(Calendar currentTime)341 public Calendar getNextAlarmTime(Calendar currentTime) { 342 final Calendar nextInstanceTime = Calendar.getInstance(); 343 nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR)); 344 nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH)); 345 nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH)); 346 nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 347 nextInstanceTime.set(Calendar.MINUTE, minutes); 348 nextInstanceTime.set(Calendar.SECOND, 0); 349 nextInstanceTime.set(Calendar.MILLISECOND, 0); 350 351 // If we are still behind the passed in currentTime, then add a day 352 if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) { 353 nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1); 354 } 355 356 // The day of the week might be invalid, so find next valid one 357 int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime); 358 if (addDays > 0) { 359 nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays); 360 } 361 return nextInstanceTime; 362 } 363 364 @Override equals(Object o)365 public boolean equals(Object o) { 366 if (!(o instanceof Alarm)) return false; 367 final Alarm other = (Alarm) o; 368 return id == other.id; 369 } 370 371 @Override hashCode()372 public int hashCode() { 373 return Long.valueOf(id).hashCode(); 374 } 375 376 @Override toString()377 public String toString() { 378 return "Alarm{" + 379 "alert=" + alert + 380 ", id=" + id + 381 ", enabled=" + enabled + 382 ", hour=" + hour + 383 ", minutes=" + minutes + 384 ", daysOfWeek=" + daysOfWeek + 385 ", vibrate=" + vibrate + 386 ", label='" + label + '\'' + 387 ", deleteAfterUse=" + deleteAfterUse + 388 '}'; 389 } 390 } 391