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