diff --git a/android-pickers/build.gradle b/android-pickers/build.gradle index c644c66ad..f33f8ca57 100644 --- a/android-pickers/build.gradle +++ b/android-pickers/build.gradle @@ -19,6 +19,11 @@ android { } } buildToolsVersion '26.0.2' + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/android-pickers/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java b/android-pickers/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java index 0bbb741e2..06c121b3c 100644 --- a/android-pickers/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java +++ b/android-pickers/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java @@ -63,6 +63,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue private static final int PULSE_ANIMATOR_DELAY = 300; private OnTimeSetListener mCallback; + private DialogInterface.OnDismissListener dismissListener; private HapticFeedbackController mHapticFeedbackController; @@ -116,7 +117,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue */ void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute); - void onTimeCleared(RadialPickerLayout view); + default void onTimeCleared(RadialPickerLayout view) {} } public TimePickerDialog() { @@ -998,4 +999,15 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue return false; } } + + public void setDismissListener( DialogInterface.OnDismissListener listener ) { + dismissListener = listener; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if( dismissListener != null ) + dismissListener.onDismiss(dialog); + } } diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index ec52f93f1..2f371d686 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -91,6 +91,11 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activities.habits.list.ListHabitsActivity"/> + + + private val listController: Lazy, + private val notificationTray: NotificationTray ) : BaseSelectionMenu() { override fun onFinish() { @@ -69,6 +74,12 @@ class ListHabitsSelectionMenu @Inject constructor( return true } + R.id.action_notify -> { + for(h in listAdapter.selected) + notificationTray.show(h, DateUtils.getToday(), 0) + return true + } + else -> return false } } @@ -78,12 +89,14 @@ class ListHabitsSelectionMenu @Inject constructor( val itemColor = menu.findItem(R.id.action_color) val itemArchive = menu.findItem(R.id.action_archive_habit) val itemUnarchive = menu.findItem(R.id.action_unarchive_habit) + val itemNotify = menu.findItem(R.id.action_notify) itemColor.isVisible = true itemEdit.isVisible = behavior.canEdit() itemArchive.isVisible = behavior.canArchive() itemUnarchive.isVisible = behavior.canUnarchive() setTitle(Integer.toString(listAdapter.selected.size)) + itemNotify.isVisible = prefs.isDeveloper return true } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java index eeb11c6aa..c74b412da 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java @@ -26,6 +26,7 @@ import org.isoron.uhabits.*; import org.isoron.uhabits.activities.common.dialogs.*; import org.isoron.uhabits.activities.habits.edit.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.screens.habits.show.*; import javax.inject.*; @@ -45,6 +46,9 @@ public class ShowHabitScreen extends BaseScreen @NonNull private final EditHabitDialogFactory editHabitDialogFactory; + @NonNull + private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; + private final Lazy behavior; @Inject @@ -52,8 +56,8 @@ public class ShowHabitScreen extends BaseScreen @NonNull Habit habit, @NonNull ShowHabitRootView view, @NonNull ShowHabitsMenu menu, - @NonNull - EditHabitDialogFactory editHabitDialogFactory, + @NonNull EditHabitDialogFactory editHabitDialogFactory, + @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, @NonNull Lazy behavior) { super(activity); @@ -63,6 +67,7 @@ public class ShowHabitScreen extends BaseScreen this.habit = habit; this.behavior = behavior; this.editHabitDialogFactory = editHabitDialogFactory; + this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; view.setController(this); } @@ -116,6 +121,19 @@ public class ShowHabitScreen extends BaseScreen { case COULD_NOT_EXPORT: showMessage(R.string.could_not_export); + + case HABIT_DELETED: + showMessage(R.string.delete_habits_message); } } + + @Override + public void showDeleteConfirmationScreen(@NonNull OnConfirmedCallback callback) { + activity.showDialog(confirmDeleteDialogFactory.create(callback)); + } + + @Override + public void close() { + activity.finish(); + } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java index fe30a3e31..733db302c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java @@ -57,6 +57,10 @@ public class ShowHabitsMenu extends BaseMenu behavior.get().onExportCSV(); return true; + case R.id.action_delete: + behavior.get().onDeleteHabit(); + return true; + default: return false; } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt index 2758ba934..b60904840 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt @@ -54,6 +54,15 @@ class PendingIntentFactory }, FLAG_UPDATE_CURRENT) + fun removeRepetition(habit: Habit): PendingIntent = + PendingIntent.getBroadcast( + context, 3, + Intent(context, WidgetReceiver::class.java).apply { + action = WidgetReceiver.ACTION_REMOVE_REPETITION + data = Uri.parse(habit.uriString) + }, + FLAG_UPDATE_CURRENT) + fun showHabit(habit: Habit): PendingIntent = android.support.v4.app.TaskStackBuilder .create(context) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt index 6a25300d0..f41c18bca 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt @@ -23,6 +23,7 @@ import android.app.* import android.content.* import android.graphics.* import android.graphics.BitmapFactory.* +import android.support.annotation.* import android.support.v4.app.* import android.support.v4.app.NotificationCompat.* import org.isoron.androidbase.* @@ -43,18 +44,39 @@ class AndroidNotificationTray private val ringtoneManager: RingtoneManager ) : NotificationTray.SystemTray { + private var active = HashSet() + override fun removeNotification(id: Int) { - NotificationManagerCompat.from(context).cancel(id) + val manager = NotificationManagerCompat.from(context) + manager.cancel(id) + active.remove(id) + + // Clear the group summary notification + if(active.isEmpty()) manager.cancelAll() } override fun showNotification(habit: Habit, notificationId: Int, timestamp: Timestamp, - reminderTime: Long) { + reminderTime: Long) + { + val notificationManager = NotificationManagerCompat.from(context) + val summary = buildSummary(reminderTime) + notificationManager.notify(Int.MAX_VALUE, summary) + val notification = buildNotification(habit, reminderTime, timestamp) + notificationManager.notify(notificationId, notification) + active.add(notificationId) + } - val checkAction = Action( + @NonNull + fun buildNotification(@NonNull habit: Habit, + @NonNull reminderTime: Long, + @NonNull timestamp: Timestamp) : Notification + { + + val addRepetitionAction = Action( R.drawable.ic_action_check, - context.getString(R.string.check), + context.getString(R.string.yes), pendingIntents.addCheckmark(habit, timestamp)) val snoozeAction = Action( @@ -62,6 +84,11 @@ class AndroidNotificationTray context.getString(R.string.snooze), pendingIntents.snoozeNotification(habit)) + val removeRepetitionAction = Action( + R.drawable.ic_action_cancel, + context.getString(R.string.no), + pendingIntents.removeRepetition(habit)) + val wearableBg = decodeResource(context.resources, R.drawable.stripe) // Even though the set of actions is the same on the phone and @@ -69,7 +96,8 @@ class AndroidNotificationTray // WearableExtender. val wearableExtender = WearableExtender() .setBackground(wearableBg) - .addAction(checkAction) + .addAction(addRepetitionAction) + .addAction(removeRepetitionAction) .addAction(snoozeAction) val builder = NotificationCompat.Builder(context) @@ -78,20 +106,32 @@ class AndroidNotificationTray .setContentText(habit.description) .setContentIntent(pendingIntents.showHabit(habit)) .setDeleteIntent(pendingIntents.dismissNotification(habit)) - .addAction(checkAction) + .addAction(addRepetitionAction) + .addAction(removeRepetitionAction) .addAction(snoozeAction) .setSound(ringtoneManager.getURI()) .extend(wearableExtender) .setWhen(reminderTime) .setShowWhen(true) .setOngoing(preferences.shouldMakeNotificationsSticky()) + .setGroup("default") if (preferences.shouldMakeNotificationsLed()) builder.setLights(Color.RED, 1000, 1000) - val notificationManager = context.getSystemService( - Activity.NOTIFICATION_SERVICE) as NotificationManager + return builder.build() + } - notificationManager.notify(notificationId, builder.build()) + @NonNull + private fun buildSummary(@NonNull reminderTime: Long) : Notification + { + return NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(context.getString(R.string.app_name)) + .setWhen(reminderTime) + .setShowWhen(true) + .setGroup("default") + .setGroupSummary(true) + .build() } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.java b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.java new file mode 100644 index 000000000..03d26a707 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.java @@ -0,0 +1,85 @@ +package org.isoron.uhabits.notifications; + + +import android.app.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v4.app.*; +import android.text.format.*; +import android.view.*; +import android.widget.*; + +import com.android.datetimepicker.time.TimePickerDialog; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.receivers.*; + +import java.util.*; + +import static android.content.ContentUris.parseId; + +public class SnoozeDelayPickerActivity extends FragmentActivity + implements AdapterView.OnItemClickListener +{ + private Habit habit; + + private ReminderController reminderController; + + @Override + protected void onCreate(@Nullable Bundle bundle) + { + super.onCreate(bundle); + if (getIntent() == null) finish(); + if (getIntent().getData() == null) finish(); + + HabitsApplication app = (HabitsApplication) getApplicationContext(); + HabitsApplicationComponent appComponent = app.getComponent(); + reminderController = appComponent.getReminderController(); + habit = appComponent.getHabitList().getById(parseId(getIntent().getData())); + if (habit == null) finish(); + + int theme = R.style.Theme_AppCompat_Light_Dialog_Alert; + AlertDialog dialog = new AlertDialog.Builder(new ContextThemeWrapper(this, theme)) + .setTitle(R.string.select_snooze_delay) + .setItems(R.array.snooze_picker_names, null) + .create(); + + dialog.getListView().setOnItemClickListener(this); + dialog.setOnDismissListener(d -> finish()); + dialog.show(); + } + + private void showTimePicker() + { + final Calendar calendar = Calendar.getInstance(); + TimePickerDialog dialog = TimePickerDialog.newInstance( + (view, hour, minute) -> { + reminderController.onSnoozeTimePicked(habit, hour, minute); + finish(); + }, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + DateFormat.is24HourFormat(this)); + dialog.show(getSupportFragmentManager(), "timePicker"); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) + { + int[] snoozeValues = getResources().getIntArray(R.array.snooze_picker_values); + if (snoozeValues[position] >= 0) + { + reminderController.onSnoozeDelayPicked(habit, snoozeValues[position]); + finish(); + } + else showTimePicker(); + } + + @Override + public void finish() + { + super.finish(); + overridePendingTransition(0, 0); + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.java b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.java index ae88f55c9..c253064f1 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.java @@ -19,19 +19,21 @@ package org.isoron.uhabits.receivers; +import android.content.*; +import android.net.*; import android.support.annotation.*; +import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.reminders.*; import org.isoron.uhabits.core.ui.*; +import org.isoron.uhabits.core.utils.*; +import org.isoron.uhabits.notifications.*; import javax.inject.*; -import static org.isoron.uhabits.core.utils.DateUtils.applyTimezone; -import static org.isoron.uhabits.core.utils.DateUtils.getLocalTime; - -@ReceiverScope +@AppScope public class ReminderController { @NonNull @@ -40,6 +42,7 @@ public class ReminderController @NonNull private final NotificationTray notificationTray; + @NonNull private Preferences preferences; @Inject @@ -65,14 +68,25 @@ public class ReminderController reminderScheduler.scheduleAll(); } - public void onSnooze(@NonNull Habit habit) + public void onSnoozePressed(@NonNull Habit habit, final Context context) { - long snoozeInterval = preferences.getSnoozeInterval(); + long delay = preferences.getSnoozeInterval(); - long now = applyTimezone(getLocalTime()); - long reminderTime = now + snoozeInterval * 60 * 1000; + if (delay < 0) + showSnoozeDelayPicker(habit, context); + else + scheduleReminderMinutesFromNow(habit, delay); + } - reminderScheduler.schedule(habit, reminderTime); + public void onSnoozeDelayPicked(Habit habit, int delay) + { + scheduleReminderMinutesFromNow(habit, delay); + } + + public void onSnoozeTimePicked(Habit habit, int hour, int minute) + { + Long time = DateUtils.getUpcomingTimeInMillis(hour, minute); + reminderScheduler.scheduleAtTime(habit, time); notificationTray.cancel(habit); } @@ -80,4 +94,19 @@ public class ReminderController { notificationTray.cancel(habit); } + + private void scheduleReminderMinutesFromNow(Habit habit, long minutes) + { + reminderScheduler.scheduleMinutesFromNow(habit, minutes); + notificationTray.cancel(habit); + } + + private void showSnoozeDelayPicker(@NonNull Habit habit, Context context) + { + context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + Intent intent = new Intent(context, SnoozeDelayPickerActivity.class); + intent.setData(Uri.parse(habit.getUriString())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java index cb382a950..dff612345 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java @@ -20,14 +20,13 @@ package org.isoron.uhabits.receivers; import android.content.*; +import android.support.annotation.*; import android.util.*; import org.isoron.uhabits.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.utils.*; -import dagger.*; - import static android.content.ContentUris.*; /** @@ -38,30 +37,26 @@ import static android.content.ContentUris.*; public class ReminderReceiver extends BroadcastReceiver { public static final String ACTION_DISMISS_REMINDER = - "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; + "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; public static final String ACTION_SHOW_REMINDER = - "org.isoron.uhabits.ACTION_SHOW_REMINDER"; + "org.isoron.uhabits.ACTION_SHOW_REMINDER"; public static final String ACTION_SNOOZE_REMINDER = - "org.isoron.uhabits.ACTION_SNOOZE_REMINDER"; + "org.isoron.uhabits.ACTION_SNOOZE_REMINDER"; private static final String TAG = "ReminderReceiver"; @Override - public void onReceive(final Context context, Intent intent) + public void onReceive(@Nullable final Context context, @Nullable Intent intent) { - HabitsApplication app = - (HabitsApplication) context.getApplicationContext(); - - ReminderComponent component = DaggerReminderReceiver_ReminderComponent - .builder() - .habitsApplicationComponent(app.getComponent()) - .build(); + if (context == null || intent == null) return; + if (intent.getAction() == null) return; - HabitList habits = app.getComponent().getHabitList(); - ReminderController reminderController = - component.getReminderController(); + HabitsApplication app = (HabitsApplication) context.getApplicationContext(); + HabitsApplicationComponent appComponent = app.getComponent(); + HabitList habits = appComponent.getHabitList(); + ReminderController reminderController = appComponent.getReminderController(); Log.i(TAG, String.format("Received intent: %s", intent.toString())); @@ -80,7 +75,7 @@ public class ReminderReceiver extends BroadcastReceiver case ACTION_SHOW_REMINDER: if (habit == null) return; reminderController.onShowReminder(habit, - new Timestamp(timestamp), reminderTime); + new Timestamp(timestamp), reminderTime); break; case ACTION_DISMISS_REMINDER: @@ -90,7 +85,7 @@ public class ReminderReceiver extends BroadcastReceiver case ACTION_SNOOZE_REMINDER: if (habit == null) return; - reminderController.onSnooze(habit); + reminderController.onSnoozePressed(habit, context); break; case Intent.ACTION_BOOT_COMPLETED: @@ -103,11 +98,4 @@ public class ReminderReceiver extends BroadcastReceiver Log.e(TAG, "could not process intent", e); } } - - @ReceiverScope - @Component(dependencies = HabitsApplicationComponent.class) - interface ReminderComponent - { - ReminderController getReminderController(); - } } diff --git a/uhabits-android/src/main/res/drawable-hdpi/ic_action_cancel.png b/uhabits-android/src/main/res/drawable-hdpi/ic_action_cancel.png new file mode 100644 index 000000000..374fc6fc0 Binary files /dev/null and b/uhabits-android/src/main/res/drawable-hdpi/ic_action_cancel.png differ diff --git a/uhabits-android/src/main/res/drawable-mdpi/ic_action_cancel.png b/uhabits-android/src/main/res/drawable-mdpi/ic_action_cancel.png new file mode 100644 index 000000000..e7de03a06 Binary files /dev/null and b/uhabits-android/src/main/res/drawable-mdpi/ic_action_cancel.png differ diff --git a/uhabits-android/src/main/res/drawable-xhdpi/ic_action_cancel.png b/uhabits-android/src/main/res/drawable-xhdpi/ic_action_cancel.png new file mode 100644 index 000000000..dbd10906c Binary files /dev/null and b/uhabits-android/src/main/res/drawable-xhdpi/ic_action_cancel.png differ diff --git a/uhabits-android/src/main/res/drawable-xxhdpi/ic_action_cancel.png b/uhabits-android/src/main/res/drawable-xxhdpi/ic_action_cancel.png new file mode 100644 index 000000000..20591def4 Binary files /dev/null and b/uhabits-android/src/main/res/drawable-xxhdpi/ic_action_cancel.png differ diff --git a/uhabits-android/src/main/res/menu/list_habits_selection.xml b/uhabits-android/src/main/res/menu/list_habits_selection.xml index 4f274e39e..0e5cd850a 100644 --- a/uhabits-android/src/main/res/menu/list_habits_selection.xml +++ b/uhabits-android/src/main/res/menu/list_habits_selection.xml @@ -46,4 +46,9 @@ android:title="@string/delete" app:showAsAction="never"/> + + \ No newline at end of file diff --git a/uhabits-android/src/main/res/menu/show_habit.xml b/uhabits-android/src/main/res/menu/show_habit.xml index 366a53e6c..e14ce108c 100644 --- a/uhabits-android/src/main/res/menu/show_habit.xml +++ b/uhabits-android/src/main/res/menu/show_habit.xml @@ -26,6 +26,11 @@ android:title="@string/export" app:showAsAction="never"/> + + @string/interval_4_hour @string/interval_8_hour @string/interval_24_hour + @string/interval_always_ask @@ -45,8 +46,31 @@ 240 480 1440 + -1 + + @string/interval_15_minutes + @string/interval_30_minutes + @string/interval_1_hour + @string/interval_2_hour + @string/interval_4_hour + @string/interval_8_hour + @string/interval_24_hour + @string/interval_custom + + + + 15 + 30 + 60 + 120 + 240 + 480 + 1440 + -1 + + @string/every_day @string/every_week diff --git a/uhabits-android/src/main/res/values/strings.xml b/uhabits-android/src/main/res/values/strings.xml index df4cc33c5..55dc0b958 100644 --- a/uhabits-android/src/main/res/values/strings.xml +++ b/uhabits-android/src/main/res/values/strings.xml @@ -83,6 +83,8 @@ 4 hours 8 hours 24 hours + Always ask + Custom... Toggle with short press Put checkmarks with a single tap instead of press-and-hold. More convenient, but might cause accidental toggles. Snooze interval on reminders @@ -95,6 +97,7 @@ Name Settings Snooze interval + Select snooze delay Did you know? To rearrange the entries, press-and-hold on the name of the habit, then drag it to the correct place. @@ -225,4 +228,6 @@ e.g. Did you exercise today? Question Target + Yes + No \ No newline at end of file diff --git a/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.java b/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.java index 93b199a10..b17bbace5 100644 --- a/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.java +++ b/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.java @@ -70,9 +70,9 @@ public class ReminderControllerTest extends BaseAndroidJVMTest DateUtils.setFixedLocalTime(now); when(preferences.getSnoozeInterval()).thenReturn(15L); - controller.onSnooze(habit); + controller.onSnoozePressed(habit,null); - verify(reminderScheduler).schedule(habit, nowTz + 900000); + verify(reminderScheduler).scheduleMinutesFromNow(habit, 15L); verify(notificationTray).cancel(habit); } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Reminder.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Reminder.java index 870954605..8cb0d31f0 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Reminder.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Reminder.java @@ -22,8 +22,9 @@ package org.isoron.uhabits.core.models; import android.support.annotation.*; import org.apache.commons.lang3.builder.*; +import org.isoron.uhabits.core.utils.*; -import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle; +import static org.isoron.uhabits.core.utils.StringUtils.*; public final class Reminder { @@ -56,6 +57,11 @@ public final class Reminder return minute; } + public long getTimeInMillis() + { + return DateUtils.getUpcomingTimeInMillis(hour, minute); + } + @Override public boolean equals(Object o) { @@ -66,29 +72,29 @@ public final class Reminder Reminder reminder = (Reminder) o; return new EqualsBuilder() - .append(hour, reminder.hour) - .append(minute, reminder.minute) - .append(days, reminder.days) - .isEquals(); + .append(hour, reminder.hour) + .append(minute, reminder.minute) + .append(days, reminder.days) + .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(17, 37) - .append(hour) - .append(minute) - .append(days) - .toHashCode(); + .append(hour) + .append(minute) + .append(days) + .toHashCode(); } @Override public String toString() { return new ToStringBuilder(this, defaultToStringStyle()) - .append("hour", hour) - .append("minute", minute) - .append("days", days) - .toString(); + .append("hour", hour) + .append("minute", minute) + .append("days", days) + .toString(); } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java index c1d05f517..fe54af3ec 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java @@ -164,7 +164,7 @@ public class MemoryHabitList extends HabitList @Override public synchronized Iterator iterator() { - return Collections.unmodifiableCollection(list).iterator(); + return new ArrayList<>(list).iterator(); } @Override diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java index e31ed71ed..51423aac7 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java @@ -24,9 +24,6 @@ import android.support.annotation.*; import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; import javax.inject.*; @@ -55,19 +52,24 @@ public class ReminderScheduler implements CommandRunner.Listener public void onCommandExecuted(@NonNull Command command, @Nullable Long refreshKey) { - if(command instanceof ToggleRepetitionCommand) return; - if(command instanceof ChangeHabitColorCommand) return; + if (command instanceof ToggleRepetitionCommand) return; + if (command instanceof ChangeHabitColorCommand) return; scheduleAll(); } - public void schedule(@NonNull Habit habit, @Nullable Long reminderTime) + public void schedule(@NonNull Habit habit) + { + if (!habit.hasReminder()) return; + Long reminderTime = habit.getReminder().getTimeInMillis(); + scheduleAtTime(habit, reminderTime); + } + + public void scheduleAtTime(@NonNull Habit habit, @NonNull Long reminderTime) { + if (reminderTime == null) throw new IllegalArgumentException(); if (!habit.hasReminder()) return; if (habit.isArchived()) return; - Reminder reminder = habit.getReminder(); - if (reminderTime == null) reminderTime = getReminderTime(reminder); long timestamp = getStartOfDay(removeTimezone(reminderTime)); - sys.scheduleShowReminder(reminderTime, habit, timestamp); } @@ -76,7 +78,7 @@ public class ReminderScheduler implements CommandRunner.Listener HabitList reminderHabits = habitList.getFiltered(HabitMatcher.WITH_ALARM); for (Habit habit : reminderHabits) - schedule(habit, null); + schedule(habit); } public void startListening() @@ -89,19 +91,11 @@ public class ReminderScheduler implements CommandRunner.Listener commandRunner.removeListener(this); } - @NonNull - private Long getReminderTime(@NonNull Reminder reminder) + public void scheduleMinutesFromNow(Habit habit, long minutes) { - 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 += DateUtils.DAY_LENGTH; - - return applyTimezone(time); + long now = applyTimezone(getLocalTime()); + long reminderTime = now + minutes * 60 * 1000; + scheduleAtTime(habit, reminderTime); } public interface SystemScheduler diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java index 2c015b4b6..ad426f528 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java @@ -21,8 +21,10 @@ package org.isoron.uhabits.core.ui.screens.habits.show; import android.support.annotation.*; +import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.tasks.*; +import org.isoron.uhabits.core.ui.callbacks.*; import java.io.*; import java.util.*; @@ -45,18 +47,23 @@ public class ShowHabitMenuBehavior @NonNull private System system; + @NonNull + private CommandRunner commandRunner; + @Inject public ShowHabitMenuBehavior(@NonNull HabitList habitList, @NonNull Habit habit, @NonNull TaskRunner taskRunner, @NonNull Screen screen, - @NonNull System system) + @NonNull System system, + @NonNull CommandRunner commandRunner) { this.habitList = habitList; this.habit = habit; this.taskRunner = taskRunner; this.screen = screen; this.system = system; + this.commandRunner = commandRunner; } public void onEditHabit() @@ -77,9 +84,20 @@ public class ShowHabitMenuBehavior })); } + public void onDeleteHabit() + { + List selected = Collections.singletonList(habit); + + screen.showDeleteConfirmationScreen(() -> { + commandRunner.execute(new DeleteHabitsCommand(habitList, selected), + null); + screen.close(); + }); + } + public enum Message { - COULD_NOT_EXPORT + COULD_NOT_EXPORT, HABIT_DELETED } public interface Screen @@ -89,6 +107,11 @@ public class ShowHabitMenuBehavior void showMessage(Message m); void showSendFileScreen(String filename); + + void showDeleteConfirmationScreen( + @NonNull OnConfirmedCallback callback); + + void close(); } public interface System diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java index 1f9500123..d3aa800af 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java @@ -48,14 +48,15 @@ public class WidgetBehavior public void onAddRepetition(@NonNull Habit habit, Timestamp timestamp) { + notificationTray.cancel(habit); Repetition rep = habit.getRepetitions().getByTimestamp(timestamp); if (rep != null) return; performToggle(habit, timestamp); - notificationTray.cancel(habit); } public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp) { + notificationTray.cancel(habit); Repetition rep = habit.getRepetitions().getByTimestamp(timestamp); if (rep == null) return; performToggle(habit, timestamp); diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/DateUtils.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/DateUtils.java index ff52f47db..149af5969 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/DateUtils.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/DateUtils.java @@ -240,6 +240,20 @@ public abstract class DateUtils } } + public static long getUpcomingTimeInMillis(int hour, int minute) + { + Calendar calendar = DateUtils.getStartOfTodayCalendar(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, 0); + Long time = calendar.getTimeInMillis(); + + if (DateUtils.getLocalTime() > time) + time += DateUtils.DAY_LENGTH; + + return applyTimezone(time); + } + public enum TruncateField { MONTH, WEEK_NUMBER, YEAR, QUARTER diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java index 480471670..82e147e16 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java @@ -118,7 +118,7 @@ public class ReminderSchedulerTest extends BaseUnitTest @Test public void testSchedule_withoutReminder() { - reminderScheduler.schedule(habit, null); + reminderScheduler.schedule(habit); Mockito.verifyZeroInteractions(sys); } @@ -133,7 +133,8 @@ public class ReminderSchedulerTest extends BaseUnitTest long expectedCheckmarkTime, long expectedReminderTime) { - reminderScheduler.schedule(habit, atTime); + if(atTime == null) reminderScheduler.schedule(habit); + else reminderScheduler.scheduleAtTime(habit, atTime); verify(sys).scheduleShowReminder(expectedReminderTime, habit, expectedCheckmarkTime); } diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehaviorTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehaviorTest.java index 08aa1a7ea..fefade28e 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehaviorTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehaviorTest.java @@ -50,7 +50,7 @@ public class ShowHabitMenuBehaviorTest extends BaseUnitTest habit = fixtures.createShortHabit(); menu = new ShowHabitMenuBehavior(habitList, habit, taskRunner, screen, - system); + system, commandRunner); } @Test