Refactor reminder scheduling, add tests

pull/151/head
Alinson S. Xavier 9 years ago
parent 3938ae6fa8
commit d6dacfd24b

@ -24,12 +24,15 @@ import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.test.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import java.util.concurrent.*;
@ -40,6 +43,8 @@ import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class BaseAndroidTest
{
// 8:00am, January 25th, 2015 (UTC)
@ -60,6 +65,9 @@ public class BaseAndroidTest
@Inject
protected CommandRunner commandRunner;
@Inject
protected HabitLogger logger;
protected AndroidTestComponent androidTestComponent;
protected HabitFixtures fixtures;

@ -0,0 +1,66 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.os.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HabitLoggerTest extends BaseAndroidTest
{
@Test
public void testLogReminderScheduled() throws IOException
{
if (!isLogcatAvailable()) return;
long time = 1422277200000L; // 13:00 jan 26, 2015 (UTC)
Habit habit = fixtures.createEmptyHabit();
habit.setName("Write journal");
logger.logReminderScheduled(habit, time);
String expectedMsg = "Setting alarm (2015-01-26 130000): Wri\n";
assertLogcatContains(expectedMsg);
}
protected void assertLogcatContains(String expectedMsg) throws IOException
{
BaseSystem system = new BaseSystem(targetContext);
String logcat = system.getLogcat();
assertThat(logcat, containsString(expectedMsg));
}
protected boolean isLogcatAvailable()
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
}

@ -33,7 +33,7 @@ import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
@MediumTest
public class HabitsApplicationTest extends BaseAndroidTest
{
@Test

@ -19,7 +19,10 @@
package org.isoron.uhabits;
import android.content.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.utils.*;
@ -70,4 +73,21 @@ public class AndroidModule
{
return new WidgetPreferences();
}
@Provides
@Singleton
ReminderScheduler provideReminderScheduler()
{
Context context = HabitsApplication.getContext();
IntentScheduler intentScheduler = new IntentScheduler(context);
IntentFactory intentFactory = new IntentFactory(context);
return new ReminderScheduler(intentFactory, intentScheduler);
}
@Provides
@Singleton
HabitLogger provideLogger()
{
return new HabitLogger();
}
}

@ -32,6 +32,7 @@ import org.isoron.uhabits.ui.habits.list.model.*;
import org.isoron.uhabits.ui.habits.list.views.*;
import org.isoron.uhabits.ui.habits.show.*;
import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
/**
@ -106,4 +107,6 @@ public interface BaseComponent
void inject(ReceiverActions receiverActions);
void inject(ReminderReceiver reminderReceiver);
void inject(ReminderScheduler reminderScheduler);
}

@ -0,0 +1,45 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
public class HabitLogger
{
public void logReminderScheduled(@NonNull Habit habit,
@NonNull Long reminderTime)
{
int min = Math.min(3, habit.getName().length());
String name = habit.getName().substring(0, min);
DateFormat df = DateUtils.getBackupDateFormat();
String time = df.format(new Date(reminderTime));
Log.i("ReminderHelper",
String.format("Setting alarm (%s): %s", time, name));
}
}

@ -51,7 +51,7 @@ public class HabitsApplication extends Application
@Nullable
private static Context context;
private static WidgetUpdater widgetManager;
private static WidgetUpdater widgetUpdater;
public static BaseComponent getComponent()
{
@ -63,9 +63,10 @@ public class HabitsApplication extends Application
HabitsApplication.component = component;
}
@Nullable
@NonNull
public static Context getContext()
{
if (context == null) throw new RuntimeException("context is null");
return context;
}
@ -76,21 +77,23 @@ public class HabitsApplication extends Application
}
@NonNull
public static WidgetUpdater getWidgetManager()
public static WidgetUpdater getWidgetUpdater()
{
if (widgetManager == null)
throw new RuntimeException("widgetManager is null");
if (widgetUpdater == null) throw new RuntimeException(
"widgetUpdater is null");
return widgetManager;
return widgetUpdater;
}
public static boolean isTestMode()
{
try
{
if (context != null) context
.getClassLoader()
.loadClass("org.isoron.uhabits.BaseAndroidTest");
if (context != null)
{
String testClass = "org.isoron.uhabits.BaseAndroidTest";
context.getClassLoader().loadClass(testClass);
}
return true;
}
catch (final Exception e)
@ -114,8 +117,8 @@ public class HabitsApplication extends Application
if (db.exists()) db.delete();
}
widgetManager = new WidgetUpdater(this);
widgetManager.startListening();
widgetUpdater = new WidgetUpdater(this);
widgetUpdater.startListening();
DatabaseUtils.initializeActiveAndroid();
}
@ -125,7 +128,7 @@ public class HabitsApplication extends Application
{
HabitsApplication.context = null;
ActiveAndroid.dispose();
widgetManager.stopListening();
widgetUpdater.stopListening();
super.onTerminate();
}
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.receivers;
package org.isoron.uhabits.intents;
import android.app.*;
import android.content.*;
@ -25,21 +25,23 @@ import android.net.*;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.habits.show.*;
public class PendingIntentFactory
{
public static final String BASE_URL = "content://org.isoron.uhabits/habit/";
import static android.app.PendingIntent.*;
public class IntentFactory
{
@NonNull
private final Context context;
public PendingIntentFactory(Context context)
public IntentFactory(@NonNull Context context)
{
this.context = context;
}
public PendingIntent buildAddCheckmark(Habit habit, Long timestamp)
public PendingIntent buildAddCheckmark(@NonNull Habit habit,
@Nullable Long timestamp)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, WidgetReceiver.class);
@ -47,7 +49,7 @@ public class PendingIntentFactory
checkIntent.setAction(WidgetReceiver.ACTION_ADD_REPETITION);
if (timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 1, checkIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
FLAG_UPDATE_CURRENT);
}
public PendingIntent buildDismissNotification()
@ -55,20 +57,37 @@ public class PendingIntentFactory
Intent deleteIntent = new Intent(context, ReminderReceiver.class);
deleteIntent.setAction(WidgetReceiver.ACTION_DISMISS_REMINDER);
return PendingIntent.getBroadcast(context, 0, deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
FLAG_UPDATE_CURRENT);
}
public PendingIntent buildSnoozeNotification(Habit habit)
public PendingIntent buildShowReminder(@NonNull Habit habit,
@Nullable Long reminderTime,
long timestamp)
{
Uri uri = habit.getUri();
Intent intent = new Intent(context, ReminderReceiver.class);
intent.setAction(ReminderReceiver.ACTION_SHOW_REMINDER);
intent.setData(uri);
intent.putExtra("timestamp", timestamp);
intent.putExtra("reminderTime", reminderTime);
int reqCode = ((int) (habit.getId() % Integer.MAX_VALUE)) + 1;
return PendingIntent.getBroadcast(context, reqCode, intent,
FLAG_UPDATE_CURRENT);
}
public PendingIntent buildSnoozeNotification(@NonNull Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, ReminderReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ReminderReceiver.ACTION_SNOOZE_REMINDER);
return PendingIntent.getBroadcast(context, 0, snoozeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
FLAG_UPDATE_CURRENT);
}
public PendingIntent buildToggleCheckmark(Habit habit, Long timestamp)
public PendingIntent buildToggleCheckmark(@NonNull Habit habit,
@Nullable Long timestamp)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, WidgetReceiver.class);
@ -76,16 +95,18 @@ public class PendingIntentFactory
checkIntent.setAction(WidgetReceiver.ACTION_TOGGLE_REPETITION);
if (timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 2, checkIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
FLAG_UPDATE_CURRENT);
}
public PendingIntent buildViewHabit(Habit habit)
{
Uri uri = habit.getUri();
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(Uri.parse(BASE_URL + habit.getId()));
intent.setData(uri);
return android.support.v4.app.TaskStackBuilder
.create(context.getApplicationContext())
.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
.getPendingIntent(0, FLAG_UPDATE_CURRENT);
}
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.receivers;
package org.isoron.uhabits.intents;
import android.content.*;
import android.net.*;
@ -70,7 +70,7 @@ public class IntentParser
return timestamp;
}
class CheckmarkIntentData
public class CheckmarkIntentData
{
public Habit habit;

@ -0,0 +1,55 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.intents;
import android.app.*;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import static android.app.AlarmManager.*;
import static android.content.Context.*;
public class IntentScheduler
{
private final AlarmManager manager;
public IntentScheduler(@NonNull Context context)
{
manager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
}
public void schedule(@NonNull Long timestamp, PendingIntent intent)
{
if (Build.VERSION.SDK_INT >= 23)
{
manager.setExactAndAllowWhileIdle(RTC_WAKEUP, timestamp, intent);
return;
}
if (Build.VERSION.SDK_INT >= 19)
{
manager.setExact(RTC_WAKEUP, timestamp, intent);
return;
}
manager.set(RTC_WAKEUP, timestamp, intent);
}
}

@ -115,8 +115,7 @@ public class Habit
}
/**
* Clears the reminder for a habit. This sets all the related fields to
* null.
* Clears the reminder for a habit.
*/
public void clearReminder()
{

@ -38,8 +38,8 @@ public final class Reminder
* Returns the days of the week the reminder should be shown.
* <p>
* This field can be converted to a list of booleans using the method
* DateHelper.unpackWeekdayList and converted back to an integer by using
* the method DateHelper.packWeekdayList.
* DateUtils.unpackWeekdayList and converted back to an integer by using the
* method DateUtils.packWeekdayList.
*/
public int getDays()
{

@ -29,6 +29,7 @@ import android.support.v4.app.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
@ -58,6 +59,9 @@ public class ReminderReceiver extends BroadcastReceiver
@Inject
HabitList habits;
@Inject
ReminderScheduler reminderScheduler;
public ReminderReceiver()
{
super();
@ -86,7 +90,7 @@ public class ReminderReceiver extends BroadcastReceiver
break;
case Intent.ACTION_BOOT_COMPLETED:
onActionBootCompleted(context);
onActionBootCompleted();
break;
}
}
@ -96,25 +100,25 @@ public class ReminderReceiver extends BroadcastReceiver
}
}
protected void onActionBootCompleted(Context context)
protected void onActionBootCompleted()
{
ReminderUtils.createReminderAlarms(context, habits);
reminderScheduler.schedule(habits);
}
protected void onActionShowReminder(Context context, Intent intent)
{
createNotification(context, intent);
createReminderAlarmsDelayed(context);
createReminderAlarmsDelayed();
}
private void createNotification(final Context context, final Intent intent)
{
final Uri data = intent.getData();
final Habit habit = habits.getById(ContentUris.parseId(data));
final Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime =
intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
final Long timestamp = intent.getLongExtra("timestamp",
DateUtils.getStartOfToday());
final Long reminderTime = intent.getLongExtra("reminderTime",
DateUtils.getStartOfToday());
if (habit == null) return;
@ -137,12 +141,11 @@ public class ReminderReceiver extends BroadcastReceiver
Intent contentIntent = new Intent(context, MainActivity.class);
contentIntent.setData(data);
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent,
PendingIntent contentPendingIntent = PendingIntent.getActivity(
context, 0, contentIntent,
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntentFactory intentFactory =
new PendingIntentFactory(context);
IntentFactory intentFactory = new IntentFactory(context);
PendingIntent dismissPendingIntent;
dismissPendingIntent = intentFactory.buildDismissNotification();
@ -151,15 +154,16 @@ public class ReminderReceiver extends BroadcastReceiver
PendingIntent snoozeIntentPending =
intentFactory.buildSnoozeNotification(habit);
Uri ringtoneUri = ReminderUtils.getRingtoneUri(context);
Uri ringtoneUri = RingtoneUtils.getRingtoneUri(context);
NotificationCompat.WearableExtender wearableExtender =
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(),
BitmapFactory.decodeResource(
context.getResources(),
R.drawable.stripe));
Notification notification =
new NotificationCompat.Builder(context)
Notification notification = new NotificationCompat.Builder(
context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.getName())
.setContentText(habit.getDescription())
@ -191,10 +195,11 @@ public class ReminderReceiver extends BroadcastReceiver
}.execute();
}
private void createReminderAlarmsDelayed(final Context context)
private void createReminderAlarmsDelayed()
{
new Handler().postDelayed(
() -> ReminderUtils.createReminderAlarms(context, habits), 5000);
new Handler().postDelayed(() -> {
reminderScheduler.schedule(habits);
}, 5000);
}
private void dismissNotification(Context context, Long habitId)
@ -210,15 +215,20 @@ public class ReminderReceiver extends BroadcastReceiver
private void onActionSnoozeReminder(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes =
Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
context);
long delayMinutes = Long.parseLong(
prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = habits.getById(habitId);
if (habit != null) ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
if (habit != null)
{
long reminderTime = new Date().getTime() + delayMinutes * 60 * 1000;
reminderScheduler.schedule(habit, reminderTime);
}
dismissNotification(context, habitId);
}
@ -227,11 +237,11 @@ public class ReminderReceiver extends BroadcastReceiver
if (!habit.hasReminder()) return false;
Reminder reminder = habit.getReminder();
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
Long timestamp = intent.getLongExtra("timestamp",
DateUtils.getStartOfToday());
boolean reminderDays[] =
DateUtils.unpackWeekdayList(reminder.getDays());
boolean reminderDays[] = DateUtils.unpackWeekdayList(
reminder.getDays());
int weekday = DateUtils.getWeekday(timestamp);
return reminderDays[weekday];

@ -24,6 +24,7 @@ import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import javax.inject.*;

@ -50,6 +50,9 @@ public class BaseSystem
@Inject
HabitList habitList;
@Inject
ReminderScheduler reminderScheduler;
public BaseSystem(Context context)
{
this.context = context;
@ -69,16 +72,16 @@ public class BaseSystem
@NonNull
public File dumpBugReportToFile() throws IOException
{
String date =
DateUtils.getBackupDateFormat().format(DateUtils.getLocalTime());
String date = DateUtils.getBackupDateFormat().format(
DateUtils.getLocalTime());
if (context == null) throw new RuntimeException(
"application context should not be null");
File dir = FileUtils.getFilesDir("Logs");
if (dir == null) throw new IOException("log dir should not be null");
File logFile =
new File(String.format("%s/Log %s.txt", dir.getPath(), date));
File logFile = new File(
String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(getBugReport());
output.close();
@ -102,54 +105,12 @@ public class BaseSystem
return deviceInfo + "\n" + logcat;
}
/**
* Recreates all application reminders.
*/
public void scheduleReminders()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
ReminderUtils.createReminderAlarms(context, habitList);
}
}.execute();
}
private String getDeviceInfo()
{
if (context == null) return "null context\n";
WindowManager wm =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
String.format("OS Version: %s (%s)\n",
System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
String.format("Device: %s\n", Build.DEVICE) +
String.format("Model (Product): %s (%s)\n", Build.MODEL,
Build.PRODUCT) +
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
String.format("Other tags: %s\n", Build.TAGS) +
String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()) +
String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()) +
String.format("External storage state: %s\n\n",
Environment.getExternalStorageState());
}
public String getLogcat() throws IOException
{
int maxLineCount = 250;
StringBuilder builder = new StringBuilder();
String[] command = new String[]{"logcat", "-d"};
String[] command = new String[]{ "logcat", "-d" };
Process process = Runtime.getRuntime().exec(command);
InputStreamReader in = new InputStreamReader(process.getInputStream());
@ -172,4 +133,40 @@ public class BaseSystem
return builder.toString();
}
/**
* Recreates all application reminders.
*/
public void scheduleReminders()
{
new SimpleTask(() -> reminderScheduler.schedule(habitList)).execute();
}
private String getDeviceInfo()
{
if (context == null) return "null context\n";
WindowManager wm = (WindowManager) context.getSystemService(
Context.WINDOW_SERVICE);
return String.format("App Version Name: %s\n",
BuildConfig.VERSION_NAME) +
String.format("App Version Code: %s\n",
BuildConfig.VERSION_CODE) +
String.format("OS Version: %s (%s)\n",
System.getProperty("os.version"),
Build.VERSION.INCREMENTAL) +
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
String.format("Device: %s\n", Build.DEVICE) +
String.format("Model (Product): %s (%s)\n", Build.MODEL,
Build.PRODUCT) +
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
String.format("Other tags: %s\n", Build.TAGS) +
String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()) +
String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()) +
String.format("External storage state: %s\n\n",
Environment.getExternalStorageState());
}
}

@ -176,7 +176,7 @@ public class ListHabitsController
new Handler().postDelayed(() -> {
system.scheduleReminders();
HabitsApplication.getWidgetManager().updateWidgets();
HabitsApplication.getWidgetUpdater().updateWidgets();
}, 1000);
}

@ -79,7 +79,7 @@ public class ScoreCard extends HabitCard
public void onItemSelected(int position)
{
setBucketSizeFromPosition(position);
HabitsApplication.getWidgetManager().updateWidgets();
HabitsApplication.getWidgetUpdater().updateWidgets();
refreshData();
}

@ -19,38 +19,46 @@
package org.isoron.uhabits.ui.settings;
import android.app.backup.BackupManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceFragmentCompat;
import org.isoron.uhabits.HabitsApplication;
import android.app.backup.*;
import android.content.*;
import android.os.*;
import android.support.v7.preference.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.HabitsApplication.*;
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener
{
private static int RINGTONE_REQUEST_CODE = 1;
private SharedPreferences prefs;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == RINGTONE_REQUEST_CODE)
{
RingtoneUtils.parseRingtoneData(getContext(), data);
updateRingtoneDescription();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
setResultOnPreferenceClick("importData",
HabitsApplication.RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV",
HabitsApplication.RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB",
HabitsApplication.RESULT_EXPORT_DB);
setResultOnPreferenceClick("bugReport",
HabitsApplication.RESULT_BUG_REPORT);
setResultOnPreferenceClick("importData", RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV", RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB", RESULT_EXPORT_DB);
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT);
updateRingtoneDescription();
@ -61,41 +69,38 @@ public class SettingsFragment extends PreferenceFragmentCompat
@Override
public void onCreatePreferences(Bundle bundle, String s)
{
// NOP
}
private void removePreference(String preferenceKey, String categoryKey)
@Override
public void onPause()
{
PreferenceCategory cat =
(PreferenceCategory) findPreference(categoryKey);
Preference pref = findPreference(preferenceKey);
cat.removePreference(pref);
prefs.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
private void setResultOnPreferenceClick(String key, final int result)
@Override
public boolean onPreferenceTreeClick(Preference preference)
{
Preference pref = findPreference(key);
pref.setOnPreferenceClickListener(preference -> {
getActivity().setResult(result);
getActivity().finish();
String key = preference.getKey();
if (key == null) return false;
if (key.equals("reminderSound"))
{
RingtoneUtils.startRingtonePickerActivity(this,
RINGTONE_REQUEST_CODE);
return true;
});
}
@Override
public void onResume()
{
super.onResume();
getPreferenceManager().getSharedPreferences().
registerOnSharedPreferenceChangeListener(this);
return super.onPreferenceTreeClick(preference);
}
@Override
public void onPause()
public void onResume()
{
getPreferenceManager().getSharedPreferences().
unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
super.onResume();
prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
}
@Override
@ -105,38 +110,28 @@ public class SettingsFragment extends PreferenceFragmentCompat
BackupManager.dataChanged("org.isoron.uhabits");
}
@Override
public boolean onPreferenceTreeClick(Preference preference)
{
if (preference.getKey() == null) return false;
if (preference.getKey().equals("reminderSound"))
private void removePreference(String preferenceKey, String categoryKey)
{
ReminderUtils.startRingtonePickerActivity(this,
RINGTONE_REQUEST_CODE);
return true;
}
return super.onPreferenceTreeClick(preference);
PreferenceCategory cat =
(PreferenceCategory) findPreference(categoryKey);
Preference pref = findPreference(preferenceKey);
cat.removePreference(pref);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == RINGTONE_REQUEST_CODE)
private void setResultOnPreferenceClick(String key, final int result)
{
ReminderUtils.parseRingtoneData(getContext(), data);
updateRingtoneDescription();
return;
}
super.onActivityResult(requestCode, resultCode, data);
Preference pref = findPreference(key);
pref.setOnPreferenceClickListener(preference -> {
getActivity().setResult(result);
getActivity().finish();
return true;
});
}
private void updateRingtoneDescription()
{
String ringtoneName = ReminderUtils.getRingtoneName(getContext());
if(ringtoneName == null) return;
String ringtoneName = RingtoneUtils.getRingtoneName(getContext());
if (ringtoneName == null) return;
Preference ringtonePreference = findPreference("reminderSound");
ringtonePreference.setSummary(ringtoneName);
}

@ -24,8 +24,8 @@ import android.content.*;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.widgets.views.*;
import org.isoron.uhabits.utils.*;
@ -45,7 +45,7 @@ public class CheckmarkWidget extends BaseWidget
@Override
public PendingIntent getOnClickPendingIntent(Context context)
{
PendingIntentFactory factory = new PendingIntentFactory(context);
IntentFactory factory = new IntentFactory(context);
return factory.buildToggleCheckmark(habit, null);
}

@ -24,8 +24,8 @@ import android.content.*;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.ui.widgets.views.*;
import org.isoron.uhabits.utils.*;
@ -46,7 +46,7 @@ public class FrequencyWidget extends BaseWidget
@Override
public PendingIntent getOnClickPendingIntent(Context context)
{
PendingIntentFactory factory = new PendingIntentFactory(context);
IntentFactory factory = new IntentFactory(context);
return factory.buildViewHabit(habit);
}

@ -56,7 +56,7 @@ public class HabitPickerDialog extends Activity
{
Long habitId = habitIds.get(position);
preferences.addWidget(widgetId, habitId);
HabitsApplication.getWidgetManager().updateWidgets();
HabitsApplication.getWidgetUpdater().updateWidgets();
Intent resultValue = new Intent();
resultValue.putExtra(EXTRA_APPWIDGET_ID, widgetId);

@ -24,8 +24,8 @@ import android.content.*;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.ui.widgets.views.*;
import org.isoron.uhabits.utils.*;
@ -44,7 +44,7 @@ public class HistoryWidget extends BaseWidget
@Override
public PendingIntent getOnClickPendingIntent(Context context)
{
PendingIntentFactory factory = new PendingIntentFactory(context);
IntentFactory factory = new IntentFactory(context);
return factory.buildViewHabit(habit);
}

@ -24,8 +24,8 @@ import android.content.*;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.ui.habits.show.views.*;
import org.isoron.uhabits.ui.widgets.views.*;
@ -47,7 +47,7 @@ public class ScoreWidget extends BaseWidget
@Override
public PendingIntent getOnClickPendingIntent(Context context)
{
PendingIntentFactory factory = new PendingIntentFactory(context);
IntentFactory factory = new IntentFactory(context);
return factory.buildViewHabit(habit);
}

@ -25,8 +25,8 @@ import android.support.annotation.*;
import android.view.*;
import android.view.ViewGroup.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.ui.widgets.views.*;
import org.isoron.uhabits.utils.*;
@ -49,7 +49,7 @@ public class StreakWidget extends BaseWidget
@Override
public PendingIntent getOnClickPendingIntent(Context context)
{
PendingIntentFactory factory = new PendingIntentFactory(context);
IntentFactory factory = new IntentFactory(context);
return factory.buildViewHabit(habit);
}

@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils;
import android.app.*;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import java.util.*;
import javax.inject.*;
import static org.isoron.uhabits.utils.DateUtils.*;
public class ReminderScheduler
{
private final IntentFactory intentFactory;
private final IntentScheduler intentScheduler;
@Inject
HabitLogger logger;
public ReminderScheduler(IntentFactory intentFactory,
IntentScheduler intentScheduler)
{
this.intentFactory = intentFactory;
this.intentScheduler = intentScheduler;
HabitsApplication.getComponent().inject(this);
}
public void schedule(@NonNull Habit habit, @Nullable Long reminderTime)
{
if (!habit.hasReminder()) return;
Reminder reminder = habit.getReminder();
if (reminderTime == null) reminderTime = getReminderTime(reminder);
long timestamp = getStartOfDay(toLocalTime(reminderTime));
PendingIntent intent = intentFactory.buildShowReminder(habit,
reminderTime, timestamp);
intentScheduler.schedule(reminderTime, intent);
logger.logReminderScheduled(habit, reminderTime);
}
public void schedule(@NonNull HabitList habits)
{
HabitList reminderHabits = habits.getFiltered(HabitMatcher.WITH_ALARM);
for (Habit habit : reminderHabits)
schedule(habit, null);
}
@NonNull
private Long getReminderTime(@NonNull Reminder reminder)
{
Calendar calendar = DateUtils.getStartOfTodayCalendar();
calendar.set(Calendar.HOUR_OF_DAY, reminder.getHour());
calendar.set(Calendar.MINUTE, reminder.getMinute());
calendar.set(Calendar.SECOND, 0);
Long time = calendar.getTimeInMillis();
if (DateUtils.getLocalTime() > time)
time += AlarmManager.INTERVAL_DAY;
return time;
}
}

@ -19,87 +19,18 @@
package org.isoron.uhabits.utils;
import android.app.*;
import android.content.*;
import android.media.*;
import android.net.*;
import android.os.*;
import android.preference.*;
import android.provider.*;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.util.*;
import android.support.v4.app.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.receivers.*;
import java.text.*;
import java.util.*;
public abstract class ReminderUtils
public abstract class RingtoneUtils
{
public static void createReminderAlarm(Context context,
Habit habit,
@Nullable Long reminderTime)
{
if (!habit.hasReminder()) return;
Reminder reminder = habit.getReminder();
if (reminderTime == null)
{
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, reminder.getHour());
calendar.set(Calendar.MINUTE, reminder.getMinute());
calendar.set(Calendar.SECOND, 0);
reminderTime = calendar.getTimeInMillis();
if (System.currentTimeMillis() > reminderTime)
reminderTime += AlarmManager.INTERVAL_DAY;
}
long timestamp =
DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime));
Uri uri = habit.getUri();
Intent alarmIntent = new Intent(context, ReminderReceiver.class);
alarmIntent.setAction(ReminderReceiver.ACTION_SHOW_REMINDER);
alarmIntent.setData(uri);
alarmIntent.putExtra("timestamp", timestamp);
alarmIntent.putExtra("reminderTime", reminderTime);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
((int) (habit.getId() % Integer.MAX_VALUE)) + 1, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager manager =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= 23)
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
reminderTime, pendingIntent);
else if (Build.VERSION.SDK_INT >= 19)
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime,
pendingIntent);
else manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
String name =
habit.getName().substring(0, Math.min(3, habit.getName().length()));
Log.d("ReminderHelper", String.format("Setting alarm (%s): %s",
DateFormat.getDateTimeInstance().format(new Date(reminderTime)),
name));
}
public static void createReminderAlarms(Context context, HabitList habits)
{
HabitList reminderHabits = habits.getFiltered(HabitMatcher.WITH_ALARM);
for (Habit habit : reminderHabits)
createReminderAlarm(context, habit, null);
}
@Nullable
public static String getRingtoneName(Context context)
{
@ -174,7 +105,7 @@ public abstract class ReminderUtils
int requestCode)
{
Uri existingRingtoneUri =
ReminderUtils.getRingtoneUri(fragment.getContext());
getRingtoneUri(fragment.getContext());
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);

@ -36,11 +36,14 @@ public class BaseUnitTest
@Inject
protected ModelFactory modelFactory;
protected TestComponent testComponent;
@Inject
protected HabitList habitList;
@Inject
protected HabitLogger logger;
protected TestComponent testComponent;
protected HabitFixtures fixtures;
@Before

@ -73,4 +73,18 @@ public class TestModule
{
return mock(WidgetPreferences.class);
}
@Provides
@Singleton
ReminderScheduler provideReminderScheduler()
{
return mock(ReminderScheduler.class);
}
@Provides
@Singleton
HabitLogger provideLogger()
{
return mock(HabitLogger.class);
}
}

@ -0,0 +1,139 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils;
import android.app.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.junit.*;
import static org.mockito.Mockito.*;
@SuppressWarnings("JavaDoc")
public class ReminderSchedulerTest extends BaseUnitTest
{
private IntentFactory intentFactory;
private IntentScheduler intentScheduler;
private ReminderScheduler scheduler;
private Habit habit;
private PendingIntent intent;
@Before
@Override
public void setUp()
{
super.setUp();
intentFactory = mock(IntentFactory.class);
intentScheduler = mock(IntentScheduler.class);
intent = mock(PendingIntent.class);
scheduler = new ReminderScheduler(intentFactory, intentScheduler);
habit = fixtures.createEmptyHabit();
}
@Test
public void testSchedule_atSpecificTime()
{
long atTime = 1422617400000L; // 11:30 jan 30, 2015 (UTC)
long expectedCheckmarkTime = 1422576000000L; // 00:00 jan 27, 2015 (UTC)
habit.setReminder(new Reminder(8, 30, DateUtils.ALL_WEEK_DAYS));
scheduleAndVerify(atTime, expectedCheckmarkTime, atTime);
}
@Test
public void testSchedule_laterToday()
{
long now = 1422253800000L; // 06:30 jan 26, 2015 (UTC)
DateUtils.setFixedLocalTime(now);
long expectedCheckmarkTime = 1422230400000L; // 00:00 jan 26, 2015 (UTC)
long expectedReminderTime = 1422261000000L; // 08:30 jan 26, 2015 (UTC)
habit.setReminder(new Reminder(8, 30, DateUtils.ALL_WEEK_DAYS));
scheduleAndVerify(null, expectedCheckmarkTime, expectedReminderTime);
}
@Test
public void testSchedule_list()
{
long now = 1422277200000L; // 13:00 jan 26, 2015 (UTC)
DateUtils.setFixedLocalTime(now);
fixtures.purgeHabits();
Habit h1 = fixtures.createEmptyHabit();
h1.setReminder(new Reminder(8, 30, DateUtils.ALL_WEEK_DAYS));
Habit h2 = fixtures.createEmptyHabit();
h2.setReminder(new Reminder(18, 30, DateUtils.ALL_WEEK_DAYS));
fixtures.createEmptyHabit();
scheduler.schedule(habitList);
verify(intentScheduler).schedule(1422347400000L, null);
verify(intentScheduler).schedule(1422297000000L, null);
verifyNoMoreInteractions(intentScheduler);
}
@Test
public void testSchedule_tomorrow()
{
long now = 1453813200000L; // 13:00 jan 26, 2016 (UTC)
DateUtils.setFixedLocalTime(now);
long expectedCheckmarkTime = 1453852800000L; // 00:00 jan 27, 2016 (UTC)
long expectedReminderTime = 1453883400000L; // 08:30 jan 27, 2016 (UTC)
habit.setReminder(new Reminder(8, 30, DateUtils.ALL_WEEK_DAYS));
scheduleAndVerify(null, expectedCheckmarkTime, expectedReminderTime);
}
@Test
public void testSchedule_withoutReminder()
{
scheduler.schedule(habit, null);
verifyZeroInteractions(intentScheduler);
}
private void scheduleAndVerify(Long atTime,
long expectedCheckmarkTime,
long expectedReminderTime)
{
when(intentFactory.buildShowReminder(habit, expectedReminderTime,
expectedCheckmarkTime)).thenReturn(intent);
scheduler.schedule(habit, atTime);
verify(logger).logReminderScheduled(habit, expectedReminderTime);
verify(intentFactory).buildShowReminder(habit, expectedReminderTime,
expectedCheckmarkTime);
verify(intentScheduler).schedule(expectedReminderTime, intent);
}
}
Loading…
Cancel
Save