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 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 102 public static Intent createIntent(String action, long alarmId) { 103 return new Intent(action).setData(getUri(alarmId)); 104 } 105 106 public static Intent createIntent(Context context, Class<?> cls, long alarmId) { 107 return new Intent(context, cls).setData(getUri(alarmId)); 108 } 109 110 public static Uri getUri(long alarmId) { 111 return ContentUris.withAppendedId(CONTENT_URI, alarmId); 112 } 113 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 */ 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 */ 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 */ 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 188 public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) { 189 ContentValues values = createContentValues(alarm); 190 Uri uri = contentResolver.insert(CONTENT_URI, values); 191 alarm.id = getId(uri); 192 return alarm; 193 } 194 195 public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) { 196 if (alarm.id == Alarm.INVALID_ID) return false; 197 ContentValues values = createContentValues(alarm); 198 long rowsUpdated = contentResolver.update(getUri(alarm.id), values, null, null); 199 return rowsUpdated == 1; 200 } 201 202 public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) { 203 if (alarmId == INVALID_ID) return false; 204 int deletedRows = contentResolver.delete(getUri(alarmId), "", null); 205 return deletedRows == 1; 206 } 207 208 public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() { 209 public Alarm createFromParcel(Parcel p) { 210 return new Alarm(p); 211 } 212 213 public Alarm[] newArray(int size) { 214 return new Alarm[size]; 215 } 216 }; 217 218 // Public fields 219 // TODO: Refactor instance names 220 public long id; 221 public boolean enabled; 222 public int hour; 223 public int minutes; 224 public DaysOfWeek daysOfWeek; 225 public boolean vibrate; 226 public String label; 227 public Uri alert; 228 public boolean deleteAfterUse; 229 230 // Creates a default alarm at the current time. 231 public Alarm() { 232 this(0, 0); 233 } 234 235 public Alarm(int hour, int minutes) { 236 this.id = INVALID_ID; 237 this.hour = hour; 238 this.minutes = minutes; 239 this.vibrate = true; 240 this.daysOfWeek = new DaysOfWeek(0); 241 this.label = ""; 242 this.alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 243 this.deleteAfterUse = false; 244 } 245 246 public Alarm(Cursor c) { 247 id = c.getLong(ID_INDEX); 248 enabled = c.getInt(ENABLED_INDEX) == 1; 249 hour = c.getInt(HOUR_INDEX); 250 minutes = c.getInt(MINUTES_INDEX); 251 daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX)); 252 vibrate = c.getInt(VIBRATE_INDEX) == 1; 253 label = c.getString(LABEL_INDEX); 254 deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1; 255 256 if (c.isNull(RINGTONE_INDEX)) { 257 // Should we be saving this with the current ringtone or leave it null 258 // so it changes when user changes default ringtone? 259 alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 260 } else { 261 alert = Uri.parse(c.getString(RINGTONE_INDEX)); 262 } 263 } 264 265 Alarm(Parcel p) { 266 id = p.readLong(); 267 enabled = p.readInt() == 1; 268 hour = p.readInt(); 269 minutes = p.readInt(); 270 daysOfWeek = new DaysOfWeek(p.readInt()); 271 vibrate = p.readInt() == 1; 272 label = p.readString(); 273 alert = (Uri) p.readParcelable(null); 274 deleteAfterUse = p.readInt() == 1; 275 } 276 277 public String getLabelOrDefault(Context context) { 278 if (label == null || label.length() == 0) { 279 return context.getString(R.string.default_label); 280 } 281 return label; 282 } 283 284 public void writeToParcel(Parcel p, int flags) { 285 p.writeLong(id); 286 p.writeInt(enabled ? 1 : 0); 287 p.writeInt(hour); 288 p.writeInt(minutes); 289 p.writeInt(daysOfWeek.getBitSet()); 290 p.writeInt(vibrate ? 1 : 0); 291 p.writeString(label); 292 p.writeParcelable(alert, flags); 293 p.writeInt(deleteAfterUse ? 1 : 0); 294 } 295 296 public int describeContents() { 297 return 0; 298 } 299 300 public AlarmInstance createInstanceAfter(Calendar time) { 301 Calendar nextInstanceTime = Calendar.getInstance(); 302 nextInstanceTime.set(Calendar.YEAR, time.get(Calendar.YEAR)); 303 nextInstanceTime.set(Calendar.MONTH, time.get(Calendar.MONTH)); 304 nextInstanceTime.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH)); 305 nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 306 nextInstanceTime.set(Calendar.MINUTE, minutes); 307 nextInstanceTime.set(Calendar.SECOND, 0); 308 nextInstanceTime.set(Calendar.MILLISECOND, 0); 309 310 // If we are still behind the passed in time, then add a day 311 if (nextInstanceTime.getTimeInMillis() <= time.getTimeInMillis()) { 312 nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1); 313 } 314 315 // The day of the week might be invalid, so find next valid one 316 int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime); 317 if (addDays > 0) { 318 nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays); 319 } 320 321 AlarmInstance result = new AlarmInstance(nextInstanceTime, id); 322 result.mVibrate = vibrate; 323 result.mLabel = label; 324 result.mRingtone = alert; 325 return result; 326 } 327 328 @Override 329 public boolean equals(Object o) { 330 if (!(o instanceof Alarm)) return false; 331 final Alarm other = (Alarm) o; 332 return id == other.id; 333 } 334 335 @Override 336 public int hashCode() { 337 return Long.valueOf(id).hashCode(); 338 } 339 340 @Override 341 public String toString() { 342 return "Alarm{" + 343 "alert=" + alert + 344 ", id=" + id + 345 ", enabled=" + enabled + 346 ", hour=" + hour + 347 ", minutes=" + minutes + 348 ", daysOfWeek=" + daysOfWeek + 349 ", vibrate=" + vibrate + 350 ", label='" + label + '\'' + 351 ", deleteAfterUse=" + deleteAfterUse + 352 '}'; 353 } 354 } 355