diff --git a/app/src/main/java/com/philliphsu/clock2/BaseFragment.java b/app/src/main/java/com/philliphsu/clock2/BaseFragment.java new file mode 100644 index 0000000..2d1dbf9 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/BaseFragment.java @@ -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(); +} diff --git a/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java b/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java index f32bc16..f9204f2 100644 --- a/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java +++ b/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java @@ -59,13 +59,15 @@ public class OnBootUpAlarmScheduler extends IntentService { @Override protected void onHandleIntent(Intent intent) { if (intent != null) { - // IntentService already works in a background thread, so we don't need to use a loader. - AlarmCursor cursor = DatabaseManager.getInstance(this).queryAlarms(); + // IntentService works in a background thread, so this won't hold us up. + AlarmCursor cursor = DatabaseManager.getInstance(this).queryEnabledAlarms(); while (cursor.moveToNext()) { Alarm alarm = cursor.getAlarm(); - if (alarm.isEnabled()) { - AlarmUtils.scheduleAlarm(this, alarm, false); + if (!alarm.isEnabled()) { + throw new IllegalStateException( + "queryEnabledAlarms() returned alarm(s) that aren't enabled"); } + AlarmUtils.scheduleAlarm(this, alarm, false); } cursor.close(); diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index 1554022..a36cdf3 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -159,6 +159,7 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, @Override public void onClick(View v) { DatabaseManager.getInstance(getActivity()).insertAlarm(item); + getLoaderManager().restartLoader(0, null, AlarmsFragment.this); if (item.isEnabled()) { AlarmUtils.scheduleAlarm(getActivity(), item, true); } diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java index 783bbd1..08403f9 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java @@ -32,6 +32,8 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper { // and defines all the columns. // TODO: Consider defining index constants for each column, // 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 COLUMN_ID = "_id"; private static final String COLUMN_HOUR = "hour"; @@ -149,8 +151,16 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper { public AlarmCursor queryAlarms() { // 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, - null, null, null, null, null, SORT_ORDER); + null, where, null, null, null, SORT_ORDER); return new AlarmCursor(c); } diff --git a/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java b/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java index b709f81..5e6ea19 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java +++ b/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java @@ -79,4 +79,8 @@ public class DatabaseManager { public AlarmCursor queryAlarms() { return mHelper.queryAlarms(); } + + public AlarmCursor queryEnabledAlarms() { + return mHelper.queryEnabledAlarms(); + } } diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java index 745b2c4..22526df 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -5,28 +5,27 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; +import android.support.v4.content.Loader; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AppCompatActivity; -import android.util.Log; import android.view.View; import android.view.WindowManager; import android.widget.Button; import com.philliphsu.clock2.Alarm; 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.LocalBroadcastHelper; -import static com.philliphsu.clock2.util.Preconditions.checkNotNull; - /** * An example full-screen activity that shows and hides the system UI (i.e. * status bar and navigation/system bar) with user interaction. * * 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 { private static final String TAG = "RingtoneActivity"; // Shared with RingtoneService @@ -35,6 +34,7 @@ public class RingtoneActivity extends AppCompatActivity { private static boolean sIsAlive = false; + private long mAlarmId; private Alarm mAlarm; @Override @@ -48,19 +48,17 @@ public class RingtoneActivity extends AppCompatActivity { getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - long id = getIntent().getLongExtra(EXTRA_ITEM_ID, -1); - if (id < 0) { + mAlarmId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1); + if (mAlarmId < 0) { throw new IllegalStateException("Cannot start RingtoneActivity without item's id"); } - mAlarm = checkNotNull(DatabaseManager.getInstance(this).getAlarm(id)); - Log.d(TAG, "Ringing alarm " + mAlarm); - - // 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); + // The reason we don't use a thread to load the alarm is because this is an + // Activity, which has complex lifecycle. LoaderManager is designed to help + // us through the vagaries of the lifecycle that could affect loading data. + getSupportLoaderManager().initLoader(0, null, this); Intent intent = new Intent(this, RingtoneService.class) - .putExtra(EXTRA_ITEM_ID, id); + .putExtra(EXTRA_ITEM_ID, mAlarmId); startService(intent); // TODO: Butterknife binding @@ -139,19 +137,44 @@ public class RingtoneActivity extends AppCompatActivity { sIsAlive = false; } + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new AlarmLoader(this, mAlarmId); + } + + @Override + public void onLoadFinished(Loader 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 loader) { + // Do nothing + } + public static boolean isAlive() { return sIsAlive; } private void snooze() { - AlarmUtils.snoozeAlarm(this, mAlarm); + if (mAlarm != null) { + AlarmUtils.snoozeAlarm(this, mAlarm); + } // 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. stopAndFinish(); } 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(); } diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java index 8521b6f..11d3715 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -64,7 +64,8 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc @Override public void run() { 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(); stopSelf(); } @@ -81,18 +82,29 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc @Override 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) throw new IllegalStateException("No item id set"); - Alarm alarm = checkNotNull(DatabaseManager.getInstance(this).getAlarm(id)); - 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 { if (ACTION_SNOOZE.equals(intent.getAction())) { - AlarmUtils.snoozeAlarm(this, alarm); + AlarmUtils.snoozeAlarm(this, mAlarm); } 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 { throw new UnsupportedOperationException(); } @@ -143,9 +155,8 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc return null; } - private void playRingtone(@NonNull Alarm alarm) { + private void playRingtone() { if (mAudioManager == null && mRingtone == null) { - mAlarm = checkNotNull(alarm); // 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 // implement an abstract method that calls startForeground(). You would then call that