blob: fc8aebdb25225e02c183f468691860e667c57492 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.deskclock.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.database.Cursor;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.deskclock.R;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Weekdays;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
/**
* Alarms start with an invalid id when it hasn't been saved to the database.
*/
public static final long INVALID_ID = -1;
/**
* The default sort order for this table
*/
private static final String DEFAULT_SORT_ORDER =
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " +
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES + " ASC" + ", " +
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC";
private static final String[] QUERY_COLUMNS = {
_ID,
HOUR,
MINUTES,
DAYS_OF_WEEK,
ENABLED,
VIBRATE,
LABEL,
RINGTONE,
DELETE_AFTER_USE
};
private static final String[] QUERY_ALARMS_WITH_INSTANCES_COLUMNS = {
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE,
ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "."
+ ClockContract.InstancesColumns.ALARM_STATE,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns._ID,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.YEAR,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MONTH,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.DAY,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.HOUR,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL,
ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE
};
/**
* These save calls to cursor.getColumnIndexOrThrow()
* THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
*/
private static final int ID_INDEX = 0;
private static final int HOUR_INDEX = 1;
private static final int MINUTES_INDEX = 2;
private static final int DAYS_OF_WEEK_INDEX = 3;
private static final int ENABLED_INDEX = 4;
private static final int VIBRATE_INDEX = 5;
private static final int LABEL_INDEX = 6;
private static final int RINGTONE_INDEX = 7;
private static final int DELETE_AFTER_USE_INDEX = 8;
private static final int INSTANCE_STATE_INDEX = 9;
public static final int INSTANCE_ID_INDEX = 10;
public static final int INSTANCE_YEAR_INDEX = 11;
public static final int INSTANCE_MONTH_INDEX = 12;
public static final int INSTANCE_DAY_INDEX = 13;
public static final int INSTANCE_HOUR_INDEX = 14;
public static final int INSTANCE_MINUTE_INDEX = 15;
public static final int INSTANCE_LABEL_INDEX = 16;
public static final int INSTANCE_VIBRATE_INDEX = 17;
private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1;
public static ContentValues createContentValues(Alarm alarm) {
ContentValues values = new ContentValues(COLUMN_COUNT);
if (alarm.id != INVALID_ID) {
values.put(ClockContract.AlarmsColumns._ID, alarm.id);
}
values.put(ENABLED, alarm.enabled ? 1 : 0);
values.put(HOUR, alarm.hour);
values.put(MINUTES, alarm.minutes);
values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBits());
values.put(VIBRATE, alarm.vibrate ? 1 : 0);
values.put(LABEL, alarm.label);
values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
if (alarm.alert == null) {
// We want to put null, so default alarm changes
values.putNull(RINGTONE);
} else {
values.put(RINGTONE, alarm.alert.toString());
}
return values;
}
public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
return new Intent(context, cls).setData(getContentUri(alarmId));
}
public static Uri getContentUri(long alarmId) {
return ContentUris.withAppendedId(CONTENT_URI, alarmId);
}
public static long getId(Uri contentUri) {
return ContentUris.parseId(contentUri);
}
/**
* Get alarm cursor loader for all alarms.
*
* @param context to query the database.
* @return cursor loader with all the alarms.
*/
public static CursorLoader getAlarmsCursorLoader(Context context) {
return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
@Override
public void onContentChanged() {
// There is a bug in Loader which can result in stale data if a loader is stopped
// immediately after a call to onContentChanged. As a workaround we stop the
// loader before delivering onContentChanged to ensure mContentChanged is set to
// true before forceLoad is called.
if (isStarted() && !isAbandoned()) {
stopLoading();
super.onContentChanged();
startLoading();
} else {
super.onContentChanged();
}
}
@Override
public Cursor loadInBackground() {
// Prime the ringtone title cache for later access. Most alarms will refer to
// system ringtones.
DataModel.getDataModel().loadRingtoneTitles();
return super.loadInBackground();
}
};
}
/**
* Get alarm by id.
*
* @param cr provides access to the content model
* @param alarmId for the desired alarm.
* @return alarm if found, null otherwise
*/
public static Alarm getAlarm(ContentResolver cr, long alarmId) {
try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) {
if (cursor.moveToFirst()) {
return new Alarm(cursor);
}
}
return null;
}
/**
* Get alarm for the {@code contentUri}.
*
* @param cr provides access to the content model
* @param contentUri the {@link #getContentUri deeplink} for the desired alarm
* @return instance if found, null otherwise
*/
public static Alarm getAlarm(ContentResolver cr, Uri contentUri) {
return getAlarm(cr, ContentUris.parseId(contentUri));
}
/**
* Get all alarms given conditions.
*
* @param cr provides access to the content model
* @param selection A filter declaring which rows to return, formatted as an
* SQL WHERE clause (excluding the WHERE itself). Passing null will
* return all rows for the given URI.
* @param selectionArgs You may include ?s in selection, which will be
* replaced by the values from selectionArgs, in the order that they
* appear in the selection. The values will be bound as Strings.
* @return list of alarms matching where clause or empty list if none found.
*/
public static List<Alarm> getAlarms(ContentResolver cr, String selection,
String... selectionArgs) {
final List<Alarm> result = new LinkedList<>();
try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
result.add(new Alarm(cursor));
} while (cursor.moveToNext());
}
}
return result;
}
public static boolean isTomorrow(Alarm alarm, Calendar now) {
if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) {
return false;
}
final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes;
final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
return totalAlarmMinutes <= totalNowMinutes;
}
public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
ContentValues values = createContentValues(alarm);
Uri uri = contentResolver.insert(CONTENT_URI, values);
alarm.id = getId(uri);
return alarm;
}
public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
if (alarm.id == Alarm.INVALID_ID) return false;
ContentValues values = createContentValues(alarm);
long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null);
return rowsUpdated == 1;
}
public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
if (alarmId == INVALID_ID) return false;
int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null);
return deletedRows == 1;
}
public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
public Alarm createFromParcel(Parcel p) {
return new Alarm(p);
}
public Alarm[] newArray(int size) {
return new Alarm[size];
}
};
// Public fields
// TODO: Refactor instance names
public long id;
public boolean enabled;
public int hour;
public int minutes;
public Weekdays daysOfWeek;
public boolean vibrate;
public String label;
public Uri alert;
public boolean deleteAfterUse;
public int instanceState;
public int instanceId;
// Creates a default alarm at the current time.
public Alarm() {
this(0, 0);
}
public Alarm(int hour, int minutes) {
this.id = INVALID_ID;
this.hour = hour;
this.minutes = minutes;
this.vibrate = true;
this.daysOfWeek = Weekdays.NONE;
this.label = "";
this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
this.deleteAfterUse = false;
}
public Alarm(Cursor c) {
id = c.getLong(ID_INDEX);
enabled = c.getInt(ENABLED_INDEX) == 1;
hour = c.getInt(HOUR_INDEX);
minutes = c.getInt(MINUTES_INDEX);
daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX));
vibrate = c.getInt(VIBRATE_INDEX) == 1;
label = c.getString(LABEL_INDEX);
deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
instanceState = c.getInt(INSTANCE_STATE_INDEX);
instanceId = c.getInt(INSTANCE_ID_INDEX);
}
if (c.isNull(RINGTONE_INDEX)) {
// Should we be saving this with the current ringtone or leave it null
// so it changes when user changes default ringtone?
alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
} else {
alert = Uri.parse(c.getString(RINGTONE_INDEX));
}
}
Alarm(Parcel p) {
id = p.readLong();
enabled = p.readInt() == 1;
hour = p.readInt();
minutes = p.readInt();
daysOfWeek = Weekdays.fromBits(p.readInt());
vibrate = p.readInt() == 1;
label = p.readString();
alert = p.readParcelable(null);
deleteAfterUse = p.readInt() == 1;
}
/**
* @return the deeplink that identifies this alarm
*/
public Uri getContentUri() {
return getContentUri(id);
}
public String getLabelOrDefault(Context context) {
return label.isEmpty() ? context.getString(R.string.default_label) : label;
}
/**
* Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
* HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
*/
public boolean canPreemptivelyDismiss() {
return instanceState == AlarmInstance.SNOOZE_STATE
|| instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
|| instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
|| instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
}
public void writeToParcel(Parcel p, int flags) {
p.writeLong(id);
p.writeInt(enabled ? 1 : 0);
p.writeInt(hour);
p.writeInt(minutes);
p.writeInt(daysOfWeek.getBits());
p.writeInt(vibrate ? 1 : 0);
p.writeString(label);
p.writeParcelable(alert, flags);
p.writeInt(deleteAfterUse ? 1 : 0);
}
public int describeContents() {
return 0;
}
public AlarmInstance createInstanceAfter(Calendar time) {
Calendar nextInstanceTime = getNextAlarmTime(time);
AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
result.mVibrate = vibrate;
result.mLabel = label;
result.mRingtone = alert;
return result;
}
/**
*
* @param currentTime the current time
* @return previous firing time, or null if this is a one-time alarm.
*/
public Calendar getPreviousAlarmTime(Calendar currentTime) {
final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
previousInstanceTime.set(Calendar.MINUTE, minutes);
previousInstanceTime.set(Calendar.SECOND, 0);
previousInstanceTime.set(Calendar.MILLISECOND, 0);
final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
if (subtractDays > 0) {
previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
return previousInstanceTime;
} else {
return null;
}
}
public Calendar getNextAlarmTime(Calendar currentTime) {
final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
nextInstanceTime.set(Calendar.MINUTE, minutes);
nextInstanceTime.set(Calendar.SECOND, 0);
nextInstanceTime.set(Calendar.MILLISECOND, 0);
// If we are still behind the passed in currentTime, then add a day
if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
}
// The day of the week might be invalid, so find next valid one
final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
if (addDays > 0) {
nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
}
// Daylight Savings Time can alter the hours and minutes when adjusting the day above.
// Reset the desired hour and minute now that the correct day has been chosen.
nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
nextInstanceTime.set(Calendar.MINUTE, minutes);
return nextInstanceTime;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Alarm)) return false;
final Alarm other = (Alarm) o;
return id == other.id;
}
@Override
public int hashCode() {
return Long.valueOf(id).hashCode();
}
@Override
public String toString() {
return "Alarm{" +
"alert=" + alert +
", id=" + id +
", enabled=" + enabled +
", hour=" + hour +
", minutes=" + minutes +
", daysOfWeek=" + daysOfWeek +
", vibrate=" + vibrate +
", label='" + label + '\'' +
", deleteAfterUse=" + deleteAfterUse +
'}';
}
}