Merge branch 'dev'

pull/542/head
Alinson S. Xavier 6 years ago
commit 0ef5a8dead

@ -1,5 +1,5 @@
VERSION_CODE = 46 VERSION_CODE = 47
VERSION_NAME = 1.8.3 VERSION_NAME = 1.8.4
MIN_SDK_VERSION = 21 MIN_SDK_VERSION = 21
TARGET_SDK_VERSION = 29 TARGET_SDK_VERSION = 29

@ -25,7 +25,6 @@ import android.graphics.*
import android.graphics.BitmapFactory.* import android.graphics.BitmapFactory.*
import android.os.* import android.os.*
import android.os.Build.VERSION.* import android.os.Build.VERSION.*
import android.support.annotation.*
import android.support.v4.app.* import android.support.v4.app.*
import android.support.v4.app.NotificationCompat.* import android.support.v4.app.NotificationCompat.*
import android.util.* import android.util.*
@ -38,9 +37,6 @@ import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.intents.* import org.isoron.uhabits.intents.*
import javax.inject.* import javax.inject.*
@AppScope @AppScope
class AndroidNotificationTray class AndroidNotificationTray
@Inject constructor( @Inject constructor(
@ -68,10 +64,9 @@ class AndroidNotificationTray
override fun showNotification(habit: Habit, override fun showNotification(habit: Habit,
notificationId: Int, notificationId: Int,
timestamp: Timestamp, timestamp: Timestamp,
reminderTime: Long) reminderTime: Long) {
{
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
val summary = buildSummary(reminderTime) val summary = buildSummary(habit, reminderTime)
notificationManager.notify(Int.MAX_VALUE, summary) notificationManager.notify(Int.MAX_VALUE, summary)
val notification = buildNotification(habit, reminderTime, timestamp) val notification = buildNotification(habit, reminderTime, timestamp)
createAndroidNotificationChannel(context) createAndroidNotificationChannel(context)
@ -79,20 +74,22 @@ class AndroidNotificationTray
notificationManager.notify(notificationId, notification) notificationManager.notify(notificationId, notification)
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
// Some Xiaomi phones produce a RuntimeException if custom notification sounds are used. // Some Xiaomi phones produce a RuntimeException if custom notification sounds are used.
Log.i("AndroidNotificationTray", "Failed to show notification. Retrying without sound.") Log.i("AndroidNotificationTray",
val n = buildNotification(habit, reminderTime, timestamp, disableSound = true) "Failed to show notification. Retrying without sound.")
val n = buildNotification(habit,
reminderTime,
timestamp,
disableSound = true)
notificationManager.notify(notificationId, n) notificationManager.notify(notificationId, n)
} }
active.add(notificationId) active.add(notificationId)
} }
@NonNull fun buildNotification(habit: Habit,
fun buildNotification(@NonNull habit: Habit, reminderTime: Long,
@NonNull reminderTime: Long, timestamp: Timestamp,
@NonNull timestamp: Timestamp, disableSound: Boolean = false): Notification {
disableSound: Boolean = false) : Notification
{
val addRepetitionAction = Action( val addRepetitionAction = Action(
R.drawable.ic_action_check, R.drawable.ic_action_check,
@ -114,10 +111,11 @@ class AndroidNotificationTray
.addAction(addRepetitionAction) .addAction(addRepetitionAction)
.addAction(removeRepetitionAction) .addAction(removeRepetitionAction)
val defaultText = context.getString(R.string.default_reminder_question)
val builder = NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID) val builder = NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name) .setContentTitle(habit.name)
.setContentText(habit.description) .setContentText(if(habit.description.isBlank()) defaultText else habit.description)
.setContentIntent(pendingIntents.showHabit(habit)) .setContentIntent(pendingIntents.showHabit(habit))
.setDeleteIntent(pendingIntents.dismissNotification(habit)) .setDeleteIntent(pendingIntents.dismissNotification(habit))
.addAction(addRepetitionAction) .addAction(addRepetitionAction)
@ -126,7 +124,7 @@ class AndroidNotificationTray
.setWhen(reminderTime) .setWhen(reminderTime)
.setShowWhen(true) .setShowWhen(true)
.setOngoing(preferences.shouldMakeNotificationsSticky()) .setOngoing(preferences.shouldMakeNotificationsSticky())
.setGroup("default") .setGroup("group" + habit.getId())
if (!disableSound) if (!disableSound)
builder.setSound(ringtoneManager.getURI()) builder.setSound(ringtoneManager.getURI())
@ -146,26 +144,24 @@ class AndroidNotificationTray
return builder.build() return builder.build()
} }
@NonNull private fun buildSummary(habit: Habit,
private fun buildSummary(@NonNull reminderTime: Long) : Notification reminderTime: Long): Notification {
{
return NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID) return NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(context.getString(R.string.app_name)) .setContentTitle(context.getString(R.string.app_name))
.setWhen(reminderTime) .setWhen(reminderTime)
.setShowWhen(true) .setShowWhen(true)
.setGroup("default") .setGroup("group" + habit.getId())
.setGroupSummary(true) .setGroupSummary(true)
.build() .build()
} }
companion object { companion object {
private val REMINDERS_CHANNEL_ID = "REMINDERS" private const val REMINDERS_CHANNEL_ID = "REMINDERS"
fun createAndroidNotificationChannel(context: Context) { fun createAndroidNotificationChannel(context: Context) {
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE) val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
as NotificationManager as NotificationManager
if (SDK_INT >= Build.VERSION_CODES.O) if (SDK_INT >= Build.VERSION_CODES.O) {
{
val channel = NotificationChannel(REMINDERS_CHANNEL_ID, val channel = NotificationChannel(REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder), context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT) NotificationManager.IMPORTANCE_DEFAULT)

@ -77,7 +77,6 @@ public class ReminderReceiver extends BroadcastReceiver
case ACTION_SHOW_REMINDER: case ACTION_SHOW_REMINDER:
if (habit == null) return; if (habit == null) return;
Log.d("ReminderReceiver", String.format( Log.d("ReminderReceiver", String.format(
Locale.US,
"onShowReminder habit=%d timestamp=%d reminderTime=%d", "onShowReminder habit=%d timestamp=%d reminderTime=%d",
habit.id, habit.id,
timestamp, timestamp,
@ -88,15 +87,18 @@ public class ReminderReceiver extends BroadcastReceiver
case ACTION_DISMISS_REMINDER: case ACTION_DISMISS_REMINDER:
if (habit == null) return; if (habit == null) return;
Log.d("ReminderReceiver", String.format("onDismiss habit=%d", habit.id));
reminderController.onDismiss(habit); reminderController.onDismiss(habit);
break; break;
case ACTION_SNOOZE_REMINDER: case ACTION_SNOOZE_REMINDER:
if (habit == null) return; if (habit == null) return;
Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id));
reminderController.onSnoozePressed(habit, context); reminderController.onSnoozePressed(habit, context);
break; break;
case Intent.ACTION_BOOT_COMPLETED: case Intent.ACTION_BOOT_COMPLETED:
Log.d("ReminderReceiver", "onBootCompleted");
reminderController.onBootCompleted(); reminderController.onBootCompleted();
break; break;
} }

@ -49,6 +49,8 @@ public class WidgetReceiver extends BroadcastReceiver
public static final String ACTION_TOGGLE_REPETITION = public static final String ACTION_TOGGLE_REPETITION =
"org.isoron.uhabits.ACTION_TOGGLE_REPETITION"; "org.isoron.uhabits.ACTION_TOGGLE_REPETITION";
private static final String TAG = "WidgetReceiver";
@Override @Override
public void onReceive(final Context context, Intent intent) public void onReceive(final Context context, Intent intent)
{ {
@ -64,6 +66,8 @@ public class WidgetReceiver extends BroadcastReceiver
WidgetBehavior controller = component.getWidgetController(); WidgetBehavior controller = component.getWidgetController();
Preferences prefs = app.getComponent().getPreferences(); Preferences prefs = app.getComponent().getPreferences();
Log.i(TAG, String.format("Received intent: %s", intent.toString()));
if (prefs.isSyncEnabled()) if (prefs.isSyncEnabled())
context.startService(new Intent(context, SyncService.class)); context.startService(new Intent(context, SyncService.class));
@ -75,16 +79,28 @@ public class WidgetReceiver extends BroadcastReceiver
switch (intent.getAction()) switch (intent.getAction())
{ {
case ACTION_ADD_REPETITION: case ACTION_ADD_REPETITION:
Log.d(TAG, String.format(
"onAddRepetition habit=%d timestamp=%d",
data.getHabit().getId(),
data.getTimestamp().getUnixTime()));
controller.onAddRepetition(data.getHabit(), controller.onAddRepetition(data.getHabit(),
data.getTimestamp()); data.getTimestamp());
break; break;
case ACTION_TOGGLE_REPETITION: case ACTION_TOGGLE_REPETITION:
Log.d(TAG, String.format(
"onToggleRepetition habit=%d timestamp=%d",
data.getHabit().getId(),
data.getTimestamp().getUnixTime()));
controller.onToggleRepetition(data.getHabit(), controller.onToggleRepetition(data.getHabit(),
data.getTimestamp()); data.getTimestamp());
break; break;
case ACTION_REMOVE_REPETITION: case ACTION_REMOVE_REPETITION:
Log.d(TAG, String.format(
"onRemoveRepetition habit=%d timestamp=%d",
data.getHabit().getId(),
data.getTimestamp().getUnixTime()));
controller.onRemoveRepetition(data.getHabit(), controller.onRemoveRepetition(data.getHabit(),
data.getTimestamp()); data.getTimestamp());
break; break;

@ -1,4 +1,4 @@
1.8.3 1.8.4
* Bugfixes * Bugfixes
1.8: 1.8:
* New bar chart showing number of repetitions performed each week, month or year * New bar chart showing number of repetitions performed each week, month or year

@ -243,5 +243,6 @@
<string name="widget_opacity_title">Widget opacity</string> <string name="widget_opacity_title">Widget opacity</string>
<string name="widget_opacity_description">Makes widgets more transparent or more opaque in your home screen.</string> <string name="widget_opacity_description">Makes widgets more transparent or more opaque in your home screen.</string>
<string name="first_day_of_the_week">First day of the week</string> <string name="first_day_of_the_week">First day of the week</string>
<string name="default_reminder_question">Have you completed this habit today?</string>
</resources> </resources>

@ -248,8 +248,8 @@ public class HabitsCSVExporter
continue; continue;
Timestamp currOld = h.getRepetitions().getOldest().getTimestamp(); Timestamp currOld = h.getRepetitions().getOldest().getTimestamp();
Timestamp currNew = h.getRepetitions().getNewest().getTimestamp(); Timestamp currNew = h.getRepetitions().getNewest().getTimestamp();
oldest = currOld.isOlderThan(oldest) ? oldest : currOld; oldest = currOld.isOlderThan(oldest) ? currOld : oldest;
newest = currNew.isNewerThan(newest) ? newest : currNew; newest = currNew.isNewerThan(newest) ? currNew : newest;
} }
return new Timestamp[]{oldest, newest}; return new Timestamp[]{oldest, newest};
} }

@ -47,6 +47,7 @@ public class HabitCardListCache implements CommandRunner.Listener
{ {
private int checkmarkCount; private int checkmarkCount;
@Nullable
private Task currentFetchTask; private Task currentFetchTask;
@NonNull @NonNull
@ -56,13 +57,15 @@ public class HabitCardListCache implements CommandRunner.Listener
private CacheData data; private CacheData data;
@NonNull @NonNull
private HabitList allHabits; private final HabitList allHabits;
@NonNull @NonNull
private HabitList filteredHabits; private HabitList filteredHabits;
@NonNull
private final TaskRunner taskRunner; private final TaskRunner taskRunner;
@NonNull
private final CommandRunner commandRunner; private final CommandRunner commandRunner;
@Inject @Inject
@ -70,21 +73,27 @@ public class HabitCardListCache implements CommandRunner.Listener
@NonNull CommandRunner commandRunner, @NonNull CommandRunner commandRunner,
@NonNull TaskRunner taskRunner) @NonNull TaskRunner taskRunner)
{ {
if (allHabits == null) throw new NullPointerException();
if (commandRunner == null) throw new NullPointerException();
if (taskRunner == null) throw new NullPointerException();
this.allHabits = allHabits; this.allHabits = allHabits;
this.commandRunner = commandRunner; this.commandRunner = commandRunner;
this.filteredHabits = allHabits; this.filteredHabits = allHabits;
this.taskRunner = taskRunner; this.taskRunner = taskRunner;
this.listener = new Listener() {}; this.listener = new Listener()
{
};
data = new CacheData(); data = new CacheData();
} }
public void cancelTasks() public synchronized void cancelTasks()
{ {
if (currentFetchTask != null) currentFetchTask.cancel(); if (currentFetchTask != null) currentFetchTask.cancel();
} }
public int[] getCheckmarks(long habitId) public synchronized int[] getCheckmarks(long habitId)
{ {
return data.checkmarks.get(habitId); return data.checkmarks.get(habitId);
} }
@ -102,53 +111,53 @@ public class HabitCardListCache implements CommandRunner.Listener
return data.habits.get(position); return data.habits.get(position);
} }
public int getHabitCount() public synchronized int getHabitCount()
{ {
return data.habits.size(); return data.habits.size();
} }
public HabitList.Order getOrder() public synchronized HabitList.Order getOrder()
{ {
return filteredHabits.getOrder(); return filteredHabits.getOrder();
} }
public double getScore(long habitId) public synchronized double getScore(long habitId)
{ {
return data.scores.get(habitId); return data.scores.get(habitId);
} }
public void onAttached() public synchronized void onAttached()
{ {
refreshAllHabits(); refreshAllHabits();
commandRunner.addListener(this); commandRunner.addListener(this);
} }
@Override @Override
public void onCommandExecuted(@NonNull Command command, public synchronized void onCommandExecuted(@Nullable Command command,
@Nullable Long refreshKey) @Nullable Long refreshKey)
{ {
if (refreshKey == null) refreshAllHabits(); if (refreshKey == null) refreshAllHabits();
else refreshHabit(refreshKey); else refreshHabit(refreshKey);
} }
public void onDetached() public synchronized void onDetached()
{ {
commandRunner.removeListener(this); commandRunner.removeListener(this);
} }
public void refreshAllHabits() public synchronized void refreshAllHabits()
{ {
if (currentFetchTask != null) currentFetchTask.cancel(); if (currentFetchTask != null) currentFetchTask.cancel();
currentFetchTask = new RefreshTask(); currentFetchTask = new RefreshTask();
taskRunner.execute(currentFetchTask); taskRunner.execute(currentFetchTask);
} }
public void refreshHabit(long id) public synchronized void refreshHabit(long id)
{ {
taskRunner.execute(new RefreshTask(id)); taskRunner.execute(new RefreshTask(id));
} }
public void remove(@NonNull Long id) public synchronized void remove(long id)
{ {
Habit h = data.id_to_habit.get(id); Habit h = data.id_to_habit.get(id);
if (h == null) return; if (h == null) return;
@ -162,7 +171,7 @@ public class HabitCardListCache implements CommandRunner.Listener
listener.onItemRemoved(position); listener.onItemRemoved(position);
} }
public void reorder(int from, int to) public synchronized void reorder(int from, int to)
{ {
Habit fromHabit = data.habits.get(from); Habit fromHabit = data.habits.get(from);
data.habits.remove(from); data.habits.remove(from);
@ -170,23 +179,26 @@ public class HabitCardListCache implements CommandRunner.Listener
listener.onItemMoved(from, to); listener.onItemMoved(from, to);
} }
public void setCheckmarkCount(int checkmarkCount) public synchronized void setCheckmarkCount(int checkmarkCount)
{ {
this.checkmarkCount = checkmarkCount; this.checkmarkCount = checkmarkCount;
} }
public void setFilter(HabitMatcher matcher) public synchronized void setFilter(@NonNull HabitMatcher matcher)
{ {
if (matcher == null) throw new NullPointerException();
filteredHabits = allHabits.getFiltered(matcher); filteredHabits = allHabits.getFiltered(matcher);
} }
public void setListener(@NonNull Listener listener) public synchronized void setListener(@NonNull Listener listener)
{ {
if (listener == null) throw new NullPointerException();
this.listener = listener; this.listener = listener;
} }
public void setOrder(HabitList.Order order) public synchronized void setOrder(@NonNull HabitList.Order order)
{ {
if (order == null) throw new NullPointerException();
allHabits.setOrder(order); allHabits.setOrder(order);
filteredHabits.setOrder(order); filteredHabits.setOrder(order);
refreshAllHabits(); refreshAllHabits();
@ -198,30 +210,40 @@ public class HabitCardListCache implements CommandRunner.Listener
*/ */
public interface Listener public interface Listener
{ {
default void onItemChanged(int position) {} default void onItemChanged(int position)
{
}
default void onItemInserted(int position) {} default void onItemInserted(int position)
{
}
default void onItemMoved(int oldPosition, int newPosition) {} default void onItemMoved(int oldPosition, int newPosition)
{
}
default void onItemRemoved(int position) {} default void onItemRemoved(int position)
{
}
default void onRefreshFinished() {} default void onRefreshFinished()
{
}
} }
private class CacheData private class CacheData
{ {
@NonNull @NonNull
public HashMap<Long, Habit> id_to_habit; public final HashMap<Long, Habit> id_to_habit;
@NonNull @NonNull
public List<Habit> habits; public final List<Habit> habits;
@NonNull @NonNull
public HashMap<Long, int[]> checkmarks; public final HashMap<Long, int[]> checkmarks;
@NonNull @NonNull
public HashMap<Long, Double> scores; public final HashMap<Long, Double> scores;
/** /**
* Creates a new CacheData without any content. * Creates a new CacheData without any content.
@ -234,8 +256,10 @@ public class HabitCardListCache implements CommandRunner.Listener
scores = new HashMap<>(); scores = new HashMap<>();
} }
public void copyCheckmarksFrom(@NonNull CacheData oldData) public synchronized void copyCheckmarksFrom(@NonNull CacheData oldData)
{ {
if (oldData == null) throw new NullPointerException();
int[] empty = new int[checkmarkCount]; int[] empty = new int[checkmarkCount];
for (Long id : id_to_habit.keySet()) for (Long id : id_to_habit.keySet())
@ -246,8 +270,10 @@ public class HabitCardListCache implements CommandRunner.Listener
} }
} }
public void copyScoresFrom(@NonNull CacheData oldData) public synchronized void copyScoresFrom(@NonNull CacheData oldData)
{ {
if (oldData == null) throw new NullPointerException();
for (Long id : id_to_habit.keySet()) for (Long id : id_to_habit.keySet())
{ {
if (oldData.scores.containsKey(id)) if (oldData.scores.containsKey(id))
@ -256,10 +282,11 @@ public class HabitCardListCache implements CommandRunner.Listener
} }
} }
public void fetchHabits() public synchronized void fetchHabits()
{ {
for (Habit h : filteredHabits) for (Habit h : filteredHabits)
{ {
if (h.getId() == null) continue;
habits.add(h); habits.add(h);
id_to_habit.put(h.getId(), h); id_to_habit.put(h.getId(), h);
} }
@ -269,13 +296,14 @@ public class HabitCardListCache implements CommandRunner.Listener
private class RefreshTask implements Task private class RefreshTask implements Task
{ {
@NonNull @NonNull
private CacheData newData; private final CacheData newData;
@Nullable @Nullable
private Long targetId; private final Long targetId;
private boolean isCancelled; private boolean isCancelled;
@Nullable
private TaskRunner runner; private TaskRunner runner;
public RefreshTask() public RefreshTask()
@ -292,13 +320,13 @@ public class HabitCardListCache implements CommandRunner.Listener
} }
@Override @Override
public void cancel() public synchronized void cancel()
{ {
isCancelled = true; isCancelled = true;
} }
@Override @Override
public void doInBackground() public synchronized void doInBackground()
{ {
newData.fetchHabits(); newData.fetchHabits();
newData.copyScoresFrom(data); newData.copyScoresFrom(data);
@ -307,7 +335,7 @@ public class HabitCardListCache implements CommandRunner.Listener
Timestamp dateTo = DateUtils.getToday(); Timestamp dateTo = DateUtils.getToday();
Timestamp dateFrom = dateTo.minus(checkmarkCount - 1); Timestamp dateFrom = dateTo.minus(checkmarkCount - 1);
runner.publishProgress(this, -1); if (runner != null) runner.publishProgress(this, -1);
for (int position = 0; position < newData.habits.size(); position++) for (int position = 0; position < newData.habits.size(); position++)
{ {
@ -318,35 +346,36 @@ public class HabitCardListCache implements CommandRunner.Listener
if (targetId != null && !targetId.equals(id)) continue; if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().getTodayValue()); newData.scores.put(id, habit.getScores().getTodayValue());
newData.checkmarks.put(id, habit newData.checkmarks.put(
.getCheckmarks() id,
.getValues(dateFrom, dateTo)); habit.getCheckmarks().getValues(dateFrom, dateTo));
runner.publishProgress(this, position); runner.publishProgress(this, position);
} }
} }
@Override @Override
public void onAttached(@NonNull TaskRunner runner) public synchronized void onAttached(@NonNull TaskRunner runner)
{ {
if (runner == null) throw new NullPointerException();
this.runner = runner; this.runner = runner;
} }
@Override @Override
public void onPostExecute() public synchronized void onPostExecute()
{ {
currentFetchTask = null; currentFetchTask = null;
listener.onRefreshFinished(); listener.onRefreshFinished();
} }
@Override @Override
public void onProgressUpdate(int currentPosition) public synchronized void onProgressUpdate(int currentPosition)
{ {
if (currentPosition < 0) processRemovedHabits(); if (currentPosition < 0) processRemovedHabits();
else processPosition(currentPosition); else processPosition(currentPosition);
} }
private void performInsert(Habit habit, int position) private synchronized void performInsert(Habit habit, int position)
{ {
Long id = habit.getId(); Long id = habit.getId();
data.habits.add(position, habit); data.habits.add(position, habit);
@ -356,14 +385,17 @@ public class HabitCardListCache implements CommandRunner.Listener
listener.onItemInserted(position); listener.onItemInserted(position);
} }
private void performMove(Habit habit, int fromPosition, int toPosition) private synchronized void performMove(@NonNull Habit habit,
int fromPosition,
int toPosition)
{ {
if(habit == null) throw new NullPointerException();
data.habits.remove(fromPosition); data.habits.remove(fromPosition);
data.habits.add(toPosition, habit); data.habits.add(toPosition, habit);
listener.onItemMoved(fromPosition, toPosition); listener.onItemMoved(fromPosition, toPosition);
} }
private void performUpdate(Long id, int position) private synchronized void performUpdate(long id, int position)
{ {
double oldScore = data.scores.get(id); double oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id); int[] oldCheckmarks = data.checkmarks.get(id);
@ -381,7 +413,7 @@ public class HabitCardListCache implements CommandRunner.Listener
listener.onItemChanged(position); listener.onItemChanged(position);
} }
private void processPosition(int currentPosition) private synchronized void processPosition(int currentPosition)
{ {
Habit habit = newData.habits.get(currentPosition); Habit habit = newData.habits.get(currentPosition);
Long id = habit.getId(); Long id = habit.getId();
@ -401,7 +433,7 @@ public class HabitCardListCache implements CommandRunner.Listener
} }
} }
private void processRemovedHabits() private synchronized void processRemovedHabits()
{ {
Set<Long> before = data.id_to_habit.keySet(); Set<Long> before = data.id_to_habit.keySet();
Set<Long> after = newData.id_to_habit.keySet(); Set<Long> after = newData.id_to_habit.keySet();

Loading…
Cancel
Save