diff --git a/android/gradle.properties b/android/gradle.properties index e2f499871..9fb12060f 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ -VERSION_CODE = 46 -VERSION_NAME = 1.8.3 +VERSION_CODE = 47 +VERSION_NAME = 1.8.4 MIN_SDK_VERSION = 21 TARGET_SDK_VERSION = 29 diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt index 3b45e14cc..d3cd51e0a 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt @@ -25,7 +25,6 @@ import android.graphics.* import android.graphics.BitmapFactory.* import android.os.* import android.os.Build.VERSION.* -import android.support.annotation.* import android.support.v4.app.* import android.support.v4.app.NotificationCompat.* import android.util.* @@ -38,9 +37,6 @@ import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.intents.* import javax.inject.* - - - @AppScope class AndroidNotificationTray @Inject constructor( @@ -48,7 +44,7 @@ class AndroidNotificationTray private val pendingIntents: PendingIntentFactory, private val preferences: Preferences, private val ringtoneManager: RingtoneManager -) : NotificationTray.SystemTray { + ) : NotificationTray.SystemTray { private var active = HashSet() override fun log(msg: String) { @@ -62,16 +58,15 @@ class AndroidNotificationTray active.remove(id) // Clear the group summary notification - if(active.isEmpty()) manager.cancelAll() + 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) + val summary = buildSummary(habit, reminderTime) notificationManager.notify(Int.MAX_VALUE, summary) val notification = buildNotification(habit, reminderTime, timestamp) createAndroidNotificationChannel(context) @@ -79,20 +74,22 @@ class AndroidNotificationTray notificationManager.notify(notificationId, notification) } catch (e: RuntimeException) { // Some Xiaomi phones produce a RuntimeException if custom notification sounds are used. - Log.i("AndroidNotificationTray", "Failed to show notification. Retrying without sound.") - val n = buildNotification(habit, reminderTime, timestamp, disableSound = true) + Log.i("AndroidNotificationTray", + "Failed to show notification. Retrying without sound.") + val n = buildNotification(habit, + reminderTime, + timestamp, + disableSound = true) notificationManager.notify(notificationId, n) } active.add(notificationId) } - @NonNull - fun buildNotification(@NonNull habit: Habit, - @NonNull reminderTime: Long, - @NonNull timestamp: Timestamp, - disableSound: Boolean = false) : Notification - { + fun buildNotification(habit: Habit, + reminderTime: Long, + timestamp: Timestamp, + disableSound: Boolean = false): Notification { val addRepetitionAction = Action( R.drawable.ic_action_check, @@ -114,10 +111,11 @@ class AndroidNotificationTray .addAction(addRepetitionAction) .addAction(removeRepetitionAction) + val defaultText = context.getString(R.string.default_reminder_question) val builder = NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(habit.name) - .setContentText(habit.description) + .setContentText(if(habit.description.isBlank()) defaultText else habit.description) .setContentIntent(pendingIntents.showHabit(habit)) .setDeleteIntent(pendingIntents.dismissNotification(habit)) .addAction(addRepetitionAction) @@ -126,7 +124,7 @@ class AndroidNotificationTray .setWhen(reminderTime) .setShowWhen(true) .setOngoing(preferences.shouldMakeNotificationsSticky()) - .setGroup("default") + .setGroup("group" + habit.getId()) if (!disableSound) builder.setSound(ringtoneManager.getURI()) @@ -134,41 +132,39 @@ class AndroidNotificationTray if (preferences.shouldMakeNotificationsLed()) builder.setLights(Color.RED, 1000, 1000) - if(SDK_INT < Build.VERSION_CODES.O) { + if (SDK_INT < Build.VERSION_CODES.O) { val snoozeAction = Action(R.drawable.ic_action_snooze, - context.getString(R.string.snooze), - pendingIntents.snoozeNotification(habit)) + context.getString(R.string.snooze), + pendingIntents.snoozeNotification(habit)) wearableExtender.addAction(snoozeAction) builder.addAction(snoozeAction) } builder.extend(wearableExtender) - return builder.build() + return builder.build() } - @NonNull - private fun buildSummary(@NonNull reminderTime: Long) : Notification - { + private fun buildSummary(habit: Habit, + reminderTime: Long): Notification { return NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(context.getString(R.string.app_name)) .setWhen(reminderTime) .setShowWhen(true) - .setGroup("default") + .setGroup("group" + habit.getId()) .setGroupSummary(true) .build() } companion object { - private val REMINDERS_CHANNEL_ID = "REMINDERS" + private const val REMINDERS_CHANNEL_ID = "REMINDERS" fun createAndroidNotificationChannel(context: Context) { val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE) as NotificationManager - if (SDK_INT >= Build.VERSION_CODES.O) - { + if (SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(REMINDERS_CHANNEL_ID, - context.resources.getString(R.string.reminder), - NotificationManager.IMPORTANCE_DEFAULT) + context.resources.getString(R.string.reminder), + NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java index 55be7c6c0..c320601e4 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java @@ -77,7 +77,6 @@ public class ReminderReceiver extends BroadcastReceiver case ACTION_SHOW_REMINDER: if (habit == null) return; Log.d("ReminderReceiver", String.format( - Locale.US, "onShowReminder habit=%d timestamp=%d reminderTime=%d", habit.id, timestamp, @@ -88,15 +87,18 @@ public class ReminderReceiver extends BroadcastReceiver case ACTION_DISMISS_REMINDER: if (habit == null) return; + Log.d("ReminderReceiver", String.format("onDismiss habit=%d", habit.id)); reminderController.onDismiss(habit); break; case ACTION_SNOOZE_REMINDER: if (habit == null) return; + Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id)); reminderController.onSnoozePressed(habit, context); break; case Intent.ACTION_BOOT_COMPLETED: + Log.d("ReminderReceiver", "onBootCompleted"); reminderController.onBootCompleted(); break; } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java index 7074224e8..c13d62d2d 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java @@ -38,33 +38,37 @@ import dagger.*; public class WidgetReceiver extends BroadcastReceiver { public static final String ACTION_ADD_REPETITION = - "org.isoron.uhabits.ACTION_ADD_REPETITION"; + "org.isoron.uhabits.ACTION_ADD_REPETITION"; public static final String ACTION_DISMISS_REMINDER = - "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; + "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; public static final String ACTION_REMOVE_REPETITION = - "org.isoron.uhabits.ACTION_REMOVE_REPETITION"; + "org.isoron.uhabits.ACTION_REMOVE_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 public void onReceive(final Context context, Intent intent) { HabitsApplication app = - (HabitsApplication) context.getApplicationContext(); + (HabitsApplication) context.getApplicationContext(); WidgetComponent component = DaggerWidgetReceiver_WidgetComponent - .builder() - .habitsApplicationComponent(app.getComponent()) - .build(); + .builder() + .habitsApplicationComponent(app.getComponent()) + .build(); IntentParser parser = app.getComponent().getIntentParser(); WidgetBehavior controller = component.getWidgetController(); Preferences prefs = app.getComponent().getPreferences(); - if(prefs.isSyncEnabled()) + Log.i(TAG, String.format("Received intent: %s", intent.toString())); + + if (prefs.isSyncEnabled()) context.startService(new Intent(context, SyncService.class)); try @@ -75,18 +79,30 @@ public class WidgetReceiver extends BroadcastReceiver switch (intent.getAction()) { case ACTION_ADD_REPETITION: + Log.d(TAG, String.format( + "onAddRepetition habit=%d timestamp=%d", + data.getHabit().getId(), + data.getTimestamp().getUnixTime())); controller.onAddRepetition(data.getHabit(), - data.getTimestamp()); + data.getTimestamp()); break; case ACTION_TOGGLE_REPETITION: + Log.d(TAG, String.format( + "onToggleRepetition habit=%d timestamp=%d", + data.getHabit().getId(), + data.getTimestamp().getUnixTime())); controller.onToggleRepetition(data.getHabit(), - data.getTimestamp()); + data.getTimestamp()); break; case ACTION_REMOVE_REPETITION: + Log.d(TAG, String.format( + "onRemoveRepetition habit=%d timestamp=%d", + data.getHabit().getId(), + data.getTimestamp().getUnixTime())); controller.onRemoveRepetition(data.getHabit(), - data.getTimestamp()); + data.getTimestamp()); break; } } diff --git a/android/uhabits-android/src/main/play/release-notes/en-US/alpha.txt b/android/uhabits-android/src/main/play/release-notes/en-US/alpha.txt index 51d97da8d..001be71d9 100644 --- a/android/uhabits-android/src/main/play/release-notes/en-US/alpha.txt +++ b/android/uhabits-android/src/main/play/release-notes/en-US/alpha.txt @@ -1,4 +1,4 @@ -1.8.3 +1.8.4 * Bugfixes 1.8: * New bar chart showing number of repetitions performed each week, month or year diff --git a/android/uhabits-android/src/main/res/values/strings.xml b/android/uhabits-android/src/main/res/values/strings.xml index 2a724c7da..af088952a 100644 --- a/android/uhabits-android/src/main/res/values/strings.xml +++ b/android/uhabits-android/src/main/res/values/strings.xml @@ -243,5 +243,6 @@ Widget opacity Makes widgets more transparent or more opaque in your home screen. First day of the week + Have you completed this habit today? \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java index cebf85b21..540d849ed 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java @@ -248,8 +248,8 @@ public class HabitsCSVExporter continue; Timestamp currOld = h.getRepetitions().getOldest().getTimestamp(); Timestamp currNew = h.getRepetitions().getNewest().getTimestamp(); - oldest = currOld.isOlderThan(oldest) ? oldest : currOld; - newest = currNew.isNewerThan(newest) ? newest : currNew; + oldest = currOld.isOlderThan(oldest) ? currOld : oldest; + newest = currNew.isNewerThan(newest) ? currNew : newest; } return new Timestamp[]{oldest, newest}; } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java index ec28965ee..b0b35d5be 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java @@ -47,6 +47,7 @@ public class HabitCardListCache implements CommandRunner.Listener { private int checkmarkCount; + @Nullable private Task currentFetchTask; @NonNull @@ -56,13 +57,15 @@ public class HabitCardListCache implements CommandRunner.Listener private CacheData data; @NonNull - private HabitList allHabits; + private final HabitList allHabits; @NonNull private HabitList filteredHabits; + @NonNull private final TaskRunner taskRunner; + @NonNull private final CommandRunner commandRunner; @Inject @@ -70,21 +73,27 @@ public class HabitCardListCache implements CommandRunner.Listener @NonNull CommandRunner commandRunner, @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.commandRunner = commandRunner; this.filteredHabits = allHabits; this.taskRunner = taskRunner; - this.listener = new Listener() {}; + this.listener = new Listener() + { + }; data = new CacheData(); } - public void cancelTasks() + public synchronized void cancelTasks() { if (currentFetchTask != null) currentFetchTask.cancel(); } - public int[] getCheckmarks(long habitId) + public synchronized int[] getCheckmarks(long habitId) { return data.checkmarks.get(habitId); } @@ -98,57 +107,57 @@ public class HabitCardListCache implements CommandRunner.Listener @Nullable public synchronized Habit getHabitByPosition(int position) { - if(position < 0 || position >= data.habits.size()) return null; + if (position < 0 || position >= data.habits.size()) return null; return data.habits.get(position); } - public int getHabitCount() + public synchronized int getHabitCount() { return data.habits.size(); } - public HabitList.Order getOrder() + public synchronized HabitList.Order getOrder() { return filteredHabits.getOrder(); } - public double getScore(long habitId) + public synchronized double getScore(long habitId) { return data.scores.get(habitId); } - public void onAttached() + public synchronized void onAttached() { refreshAllHabits(); commandRunner.addListener(this); } @Override - public void onCommandExecuted(@NonNull Command command, - @Nullable Long refreshKey) + public synchronized void onCommandExecuted(@Nullable Command command, + @Nullable Long refreshKey) { if (refreshKey == null) refreshAllHabits(); else refreshHabit(refreshKey); } - public void onDetached() + public synchronized void onDetached() { commandRunner.removeListener(this); } - public void refreshAllHabits() + public synchronized void refreshAllHabits() { if (currentFetchTask != null) currentFetchTask.cancel(); currentFetchTask = new RefreshTask(); taskRunner.execute(currentFetchTask); } - public void refreshHabit(long id) + public synchronized void refreshHabit(long 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); if (h == null) return; @@ -162,7 +171,7 @@ public class HabitCardListCache implements CommandRunner.Listener listener.onItemRemoved(position); } - public void reorder(int from, int to) + public synchronized void reorder(int from, int to) { Habit fromHabit = data.habits.get(from); data.habits.remove(from); @@ -170,23 +179,26 @@ public class HabitCardListCache implements CommandRunner.Listener listener.onItemMoved(from, to); } - public void setCheckmarkCount(int checkmarkCount) + public synchronized void setCheckmarkCount(int 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); } - public void setListener(@NonNull Listener listener) + public synchronized void setListener(@NonNull Listener listener) { + if (listener == null) throw new NullPointerException(); 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); filteredHabits.setOrder(order); refreshAllHabits(); @@ -198,30 +210,40 @@ public class HabitCardListCache implements CommandRunner.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 { @NonNull - public HashMap id_to_habit; + public final HashMap id_to_habit; @NonNull - public List habits; + public final List habits; @NonNull - public HashMap checkmarks; + public final HashMap checkmarks; @NonNull - public HashMap scores; + public final HashMap scores; /** * Creates a new CacheData without any content. @@ -234,8 +256,10 @@ public class HabitCardListCache implements CommandRunner.Listener 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]; 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()) { 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) { + if (h.getId() == null) continue; habits.add(h); id_to_habit.put(h.getId(), h); } @@ -269,13 +296,14 @@ public class HabitCardListCache implements CommandRunner.Listener private class RefreshTask implements Task { @NonNull - private CacheData newData; + private final CacheData newData; @Nullable - private Long targetId; + private final Long targetId; private boolean isCancelled; + @Nullable private TaskRunner runner; public RefreshTask() @@ -292,13 +320,13 @@ public class HabitCardListCache implements CommandRunner.Listener } @Override - public void cancel() + public synchronized void cancel() { isCancelled = true; } @Override - public void doInBackground() + public synchronized void doInBackground() { newData.fetchHabits(); newData.copyScoresFrom(data); @@ -307,7 +335,7 @@ public class HabitCardListCache implements CommandRunner.Listener Timestamp dateTo = DateUtils.getToday(); 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++) { @@ -318,35 +346,36 @@ public class HabitCardListCache implements CommandRunner.Listener if (targetId != null && !targetId.equals(id)) continue; newData.scores.put(id, habit.getScores().getTodayValue()); - newData.checkmarks.put(id, habit - .getCheckmarks() - .getValues(dateFrom, dateTo)); + newData.checkmarks.put( + id, + habit.getCheckmarks().getValues(dateFrom, dateTo)); runner.publishProgress(this, position); } } @Override - public void onAttached(@NonNull TaskRunner runner) + public synchronized void onAttached(@NonNull TaskRunner runner) { + if (runner == null) throw new NullPointerException(); this.runner = runner; } @Override - public void onPostExecute() + public synchronized void onPostExecute() { currentFetchTask = null; listener.onRefreshFinished(); } @Override - public void onProgressUpdate(int currentPosition) + public synchronized void onProgressUpdate(int currentPosition) { if (currentPosition < 0) processRemovedHabits(); else processPosition(currentPosition); } - private void performInsert(Habit habit, int position) + private synchronized void performInsert(Habit habit, int position) { Long id = habit.getId(); data.habits.add(position, habit); @@ -356,14 +385,17 @@ public class HabitCardListCache implements CommandRunner.Listener 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.add(toPosition, habit); 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); int[] oldCheckmarks = data.checkmarks.get(id); @@ -381,7 +413,7 @@ public class HabitCardListCache implements CommandRunner.Listener listener.onItemChanged(position); } - private void processPosition(int currentPosition) + private synchronized void processPosition(int currentPosition) { Habit habit = newData.habits.get(currentPosition); Long id = habit.getId(); @@ -401,7 +433,7 @@ public class HabitCardListCache implements CommandRunner.Listener } } - private void processRemovedHabits() + private synchronized void processRemovedHabits() { Set before = data.id_to_habit.keySet(); Set after = newData.id_to_habit.keySet();