Background loading of single Alarm in relevant components, added queryEnabledAlarms() in database helper

This commit is contained in:
Phillip Hsu 2016-07-01 03:40:27 -07:00
parent 5f138f2756
commit b1657c221e
7 changed files with 91 additions and 30 deletions

View File

@ -0,0 +1,10 @@
package com.philliphsu.clock2;
import android.app.Fragment;
/**
* Created by Phillip Hsu on 6/30/2016.
*/
public abstract class BaseFragment extends Fragment {
public abstract void onFabClick();
}

View File

@ -59,13 +59,15 @@ public class OnBootUpAlarmScheduler extends IntentService {
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
if (intent != null) { if (intent != null) {
// IntentService already works in a background thread, so we don't need to use a loader. // IntentService works in a background thread, so this won't hold us up.
AlarmCursor cursor = DatabaseManager.getInstance(this).queryAlarms(); AlarmCursor cursor = DatabaseManager.getInstance(this).queryEnabledAlarms();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
Alarm alarm = cursor.getAlarm(); Alarm alarm = cursor.getAlarm();
if (alarm.isEnabled()) { if (!alarm.isEnabled()) {
AlarmUtils.scheduleAlarm(this, alarm, false); throw new IllegalStateException(
"queryEnabledAlarms() returned alarm(s) that aren't enabled");
} }
AlarmUtils.scheduleAlarm(this, alarm, false);
} }
cursor.close(); cursor.close();

View File

@ -159,6 +159,7 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
@Override @Override
public void onClick(View v) { public void onClick(View v) {
DatabaseManager.getInstance(getActivity()).insertAlarm(item); DatabaseManager.getInstance(getActivity()).insertAlarm(item);
getLoaderManager().restartLoader(0, null, AlarmsFragment.this);
if (item.isEnabled()) { if (item.isEnabled()) {
AlarmUtils.scheduleAlarm(getActivity(), item, true); AlarmUtils.scheduleAlarm(getActivity(), item, true);
} }

View File

@ -32,6 +32,8 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
// and defines all the columns. // and defines all the columns.
// TODO: Consider defining index constants for each column, // TODO: Consider defining index constants for each column,
// and then removing all cursor getColumnIndex() calls. // and then removing all cursor getColumnIndex() calls.
// TODO: Consider making these public, so callers can customize their
// WHERE queries.
private static final String TABLE_ALARMS = "alarms"; private static final String TABLE_ALARMS = "alarms";
private static final String COLUMN_ID = "_id"; private static final String COLUMN_ID = "_id";
private static final String COLUMN_HOUR = "hour"; private static final String COLUMN_HOUR = "hour";
@ -149,8 +151,16 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
public AlarmCursor queryAlarms() { public AlarmCursor queryAlarms() {
// Select all rows and columns // Select all rows and columns
return queryAlarms(null);
}
public AlarmCursor queryEnabledAlarms() {
return queryAlarms(COLUMN_ENABLED + " = " + 1);
}
private AlarmCursor queryAlarms(String where) {
Cursor c = getReadableDatabase().query(TABLE_ALARMS, Cursor c = getReadableDatabase().query(TABLE_ALARMS,
null, null, null, null, null, SORT_ORDER); null, where, null, null, null, SORT_ORDER);
return new AlarmCursor(c); return new AlarmCursor(c);
} }

View File

@ -79,4 +79,8 @@ public class DatabaseManager {
public AlarmCursor queryAlarms() { public AlarmCursor queryAlarms() {
return mHelper.queryAlarms(); return mHelper.queryAlarms();
} }
public AlarmCursor queryEnabledAlarms() {
return mHelper.queryEnabledAlarms();
}
} }

View File

@ -5,28 +5,27 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmLoader;
import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper; import com.philliphsu.clock2.util.LocalBroadcastHelper;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
/** /**
* An example full-screen activity that shows and hides the system UI (i.e. * An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction. * status bar and navigation/system bar) with user interaction.
* *
* TODO: Make this abstract and make appropriate subclasses for Alarms and Timers. * TODO: Make this abstract and make appropriate subclasses for Alarms and Timers.
*/ */
public class RingtoneActivity extends AppCompatActivity { public class RingtoneActivity extends AppCompatActivity implements
android.support.v4.app.LoaderManager.LoaderCallbacks<Alarm> {
private static final String TAG = "RingtoneActivity"; private static final String TAG = "RingtoneActivity";
// Shared with RingtoneService // Shared with RingtoneService
@ -35,6 +34,7 @@ public class RingtoneActivity extends AppCompatActivity {
private static boolean sIsAlive = false; private static boolean sIsAlive = false;
private long mAlarmId;
private Alarm mAlarm; private Alarm mAlarm;
@Override @Override
@ -48,19 +48,17 @@ public class RingtoneActivity extends AppCompatActivity {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
long id = getIntent().getLongExtra(EXTRA_ITEM_ID, -1); mAlarmId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
if (id < 0) { if (mAlarmId < 0) {
throw new IllegalStateException("Cannot start RingtoneActivity without item's id"); throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
} }
mAlarm = checkNotNull(DatabaseManager.getInstance(this).getAlarm(id)); // The reason we don't use a thread to load the alarm is because this is an
Log.d(TAG, "Ringing alarm " + mAlarm); // Activity, which has complex lifecycle. LoaderManager is designed to help
// us through the vagaries of the lifecycle that could affect loading data.
// TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected. getSupportLoaderManager().initLoader(0, null, this);
// This could be the case if we're starting a new instance of this activity after leaving the first launch.
AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm);
Intent intent = new Intent(this, RingtoneService.class) Intent intent = new Intent(this, RingtoneService.class)
.putExtra(EXTRA_ITEM_ID, id); .putExtra(EXTRA_ITEM_ID, mAlarmId);
startService(intent); startService(intent);
// TODO: Butterknife binding // TODO: Butterknife binding
@ -139,19 +137,44 @@ public class RingtoneActivity extends AppCompatActivity {
sIsAlive = false; sIsAlive = false;
} }
@Override
public Loader<Alarm> onCreateLoader(int id, Bundle args) {
return new AlarmLoader(this, mAlarmId);
}
@Override
public void onLoadFinished(Loader<Alarm> loader, Alarm data) {
mAlarm = data;
if (mAlarm != null) {
// TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected.
// This could be the case if we're starting a new instance of this activity after leaving the first launch.
AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm);
}
}
@Override
public void onLoaderReset(Loader<Alarm> loader) {
// Do nothing
}
public static boolean isAlive() { public static boolean isAlive() {
return sIsAlive; return sIsAlive;
} }
private void snooze() { private void snooze() {
if (mAlarm != null) {
AlarmUtils.snoozeAlarm(this, mAlarm); AlarmUtils.snoozeAlarm(this, mAlarm);
}
// Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example, // Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example,
// we don't want the alarm, if it has no recurrence, to be turned off right now. // we don't want the alarm, if it has no recurrence, to be turned off right now.
stopAndFinish(); stopAndFinish();
} }
private void dismiss() { private void dismiss() {
AlarmUtils.cancelAlarm(this, mAlarm, false); // TODO do we really need to cancel the intent and alarm? if (mAlarm != null) {
// TODO do we really need to cancel the intent and alarm?
AlarmUtils.cancelAlarm(this, mAlarm, false);
}
stopAndFinish(); stopAndFinish();
} }

View File

@ -64,7 +64,8 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
@Override @Override
public void run() { public void run() {
mAutoSilenced = true; mAutoSilenced = true;
AlarmUtils.cancelAlarm(RingtoneService.this, mAlarm, false); // TODO do we really need to cancel the alarm and intent? // TODO do we really need to cancel the alarm and intent?
AlarmUtils.cancelAlarm(RingtoneService.this, mAlarm, false);
finishActivity(); finishActivity();
stopSelf(); stopSelf();
} }
@ -81,18 +82,29 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
long id = intent.getLongExtra(EXTRA_ITEM_ID, -1); final long id = intent.getLongExtra(EXTRA_ITEM_ID, -1);
if (id < 0) if (id < 0)
throw new IllegalStateException("No item id set"); throw new IllegalStateException("No item id set");
Alarm alarm = checkNotNull(DatabaseManager.getInstance(this).getAlarm(id));
if (intent.getAction() == null) { if (intent.getAction() == null) {
playRingtone(alarm); // http://stackoverflow.com/q/8696146/5055032
// Start our own thread to load the alarm instead of using a loader,
// because Services do not have a built-in LoaderManager (because they have no need for one since
// their lifecycle is not complex like in Activities/Fragments) and our
// work is simple enough that getting loaders to work here is not
// worth the effort.
new Thread(new Runnable() {
@Override
public void run() {
mAlarm = checkNotNull(DatabaseManager
.getInstance(RingtoneService.this).getAlarm(id));
playRingtone();
}
}).start();
} else { } else {
if (ACTION_SNOOZE.equals(intent.getAction())) { if (ACTION_SNOOZE.equals(intent.getAction())) {
AlarmUtils.snoozeAlarm(this, alarm); AlarmUtils.snoozeAlarm(this, mAlarm);
} else if (ACTION_DISMISS.equals(intent.getAction())) { } else if (ACTION_DISMISS.equals(intent.getAction())) {
AlarmUtils.cancelAlarm(this, alarm, false); // TODO do we really need to cancel the intent and alarm? AlarmUtils.cancelAlarm(this, mAlarm, false); // TODO do we really need to cancel the intent and alarm?
} else { } else {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -143,9 +155,8 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
return null; return null;
} }
private void playRingtone(@NonNull Alarm alarm) { private void playRingtone() {
if (mAudioManager == null && mRingtone == null) { if (mAudioManager == null && mRingtone == null) {
mAlarm = checkNotNull(alarm);
// TODO: The below call requires a notification, and there is no way to provide one suitable // TODO: The below call requires a notification, and there is no way to provide one suitable
// for both Alarms and Timers. Consider making this class abstract, and have subclasses // for both Alarms and Timers. Consider making this class abstract, and have subclasses
// implement an abstract method that calls startForeground(). You would then call that // implement an abstract method that calls startForeground(). You would then call that