diff --git a/android-pickers/build.gradle b/android-pickers/build.gradle index fc75627d8..e88f868cc 100644 --- a/android-pickers/build.gradle +++ b/android-pickers/build.gradle @@ -18,6 +18,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + 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 b7cbb4520..be3dff213 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/notifications/AndroidNotificationTray.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt index 15abc403f..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 @@ -44,8 +44,15 @@ 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, @@ -58,6 +65,7 @@ class AndroidNotificationTray notificationManager.notify(Int.MAX_VALUE, summary) val notification = buildNotification(habit, reminderTime, timestamp) notificationManager.notify(notificationId, notification) + active.add(notificationId) } @NonNull @@ -79,8 +87,7 @@ class AndroidNotificationTray val removeRepetitionAction = Action( R.drawable.ic_action_cancel, context.getString(R.string.no), - pendingIntents.removeRepetition(habit) - ) + pendingIntents.removeRepetition(habit)) val wearableBg = decodeResource(context.resources, R.drawable.stripe) 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/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/values/constants.xml b/uhabits-android/src/main/res/values/constants.xml index 94ae54340..34e8136a5 100644 --- a/uhabits-android/src/main/res/values/constants.xml +++ b/uhabits-android/src/main/res/values/constants.xml @@ -35,6 +35,7 @@ @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 bb89c2fab..5c043b111 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. 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..69528118a 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).scheduleAtTime(habit, nowTz + 900000); 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/reminders/ReminderScheduler.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java index e31ed71ed..ce4059ae8 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.*; @@ -60,14 +57,17 @@ public class ReminderScheduler implements CommandRunner.Listener scheduleAll(); } - public void schedule(@NonNull Habit habit, @Nullable Long reminderTime) + public void schedule(@NonNull Habit habit) + { + Long reminderTime = habit.getReminder().getTimeInMillis(); + scheduleAtTime(habit, reminderTime); + } + + public void scheduleAtTime(@NonNull Habit habit, @NonNull Long reminderTime) { 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 +76,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 +89,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/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