From 9d4cfbd2708680287f07b8aa48c34065eacf8117 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Mon, 25 Jan 2021 10:56:07 +0100 Subject: [PATCH 01/35] Fix preferences --- .../org/isoron/uhabits/activities/settings/SettingsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt index 38418e1b5..4187f5008 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt @@ -78,7 +78,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT) } - override fun onCreatePreferences(bundle: Bundle, s: String) { + override fun onCreatePreferences(bundle: Bundle?, s: String?) { // NOP } From f541f474766cedf66ba5db933c4766500d15531c Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 19:37:33 +0100 Subject: [PATCH 02/35] Use correct BREAK_STRATEGY_BALANCED constant --- .../uhabits/activities/habits/list/views/HabitCardView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index cb76becd4..cc2f1c254 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -20,12 +20,12 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.M import android.os.Handler import android.os.Looper -import android.text.Layout import android.text.TextUtils import android.view.Gravity import android.view.View @@ -137,7 +137,7 @@ class HabitCardView( maxLines = 2 ellipsize = TextUtils.TruncateAt.END layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f) - if (SDK_INT >= M) breakStrategy = Layout.BREAK_STRATEGY_BALANCED + if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED } checkmarkPanel = checkmarkPanelFactory.create().apply { From 68a8f9d356a85b4958ea572a35f5492cfc7aff4b Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 15:03:14 +0100 Subject: [PATCH 03/35] Enable mockito inline extension --- .../resources/mockito-extensions/org.mockito.plugins.MockMaker | 1 + 1 file changed, 1 insertion(+) create mode 100644 uhabits-core/src/jvmMain/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/uhabits-core/src/jvmMain/resources/mockito-extensions/org.mockito.plugins.MockMaker b/uhabits-core/src/jvmMain/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/uhabits-core/src/jvmMain/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline From 26fb76f95fe74033fd474d089a2579d454b937c3 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 15:21:03 +0100 Subject: [PATCH 04/35] Add mockitokotlin dependency to androidTest --- uhabits-android/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index ace9d2289..960590f66 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -85,6 +85,7 @@ dependencies { androidTestImplementation("androidx.test.ext:junit:1.1.2") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test:rules:1.3.0") + androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") compileOnly("javax.annotation:jsr250-api:1.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.1") implementation("com.github.paolorotolo:appintro:3.4.0") From e84cc8e8b1ca2268c6f969070bd42b8021524e30 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Tue, 19 Jan 2021 22:34:00 +0100 Subject: [PATCH 05/35] Convert HabitCardListCache --- .../habits/list/views/HabitCardListAdapter.kt | 2 +- .../habits/list/HabitCardListCache.java | 468 ------------------ .../screens/habits/list/HabitCardListCache.kt | 369 ++++++++++++++ 3 files changed, 370 insertions(+), 469 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 8dae13f2d..10eeb4d5b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -121,7 +121,7 @@ class HabitCardListAdapter @Inject constructor( val score = cache.getScore(habit!!.id!!) val checkmarks = cache.getCheckmarks(habit.id!!) val selected = selected.contains(habit) - listView!!.bindCardView(holder, habit, score, checkmarks, selected) + listView!!.bindCardView(holder, habit, score, checkmarks!!, selected) } override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java deleted file mode 100644 index a0eeebe6d..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.apache.commons.lang3.*; -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; - -import javax.inject.*; - -/** - * A HabitCardListCache fetches and keeps a cache of all the data necessary to - * render a HabitCardListView. - *

- * This is needed since performing database lookups during scrolling can make - * the ListView very slow. It also registers itself as an observer of the - * models, in order to update itself automatically. - *

- * Note that this class is singleton-scoped, therefore it is shared among all - * activities. - */ -@AppScope -public class HabitCardListCache implements CommandRunner.Listener -{ - private int checkmarkCount; - - @Nullable - private Task currentFetchTask; - - @NonNull - private Listener listener; - - @NonNull - private CacheData data; - - @NonNull - private final HabitList allHabits; - - @NonNull - private HabitList filteredHabits; - - @NonNull - private final TaskRunner taskRunner; - - @NonNull - private final CommandRunner commandRunner; - - @Inject - public HabitCardListCache(@NonNull HabitList allHabits, - @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() - { - }; - data = new CacheData(); - } - - public synchronized void cancelTasks() - { - if (currentFetchTask != null) currentFetchTask.cancel(); - } - - public synchronized int[] getCheckmarks(long habitId) - { - return data.checkmarks.get(habitId); - } - - /** - * Returns the habits that occupies a certain position on the list. - * - * @param position the position of the habit - * @return the habit at given position or null if position is invalid - */ - @Nullable - public synchronized Habit getHabitByPosition(int position) - { - if (position < 0 || position >= data.habits.size()) return null; - return data.habits.get(position); - } - - public synchronized int getHabitCount() - { - return data.habits.size(); - } - - public synchronized HabitList.Order getPrimaryOrder() - { - return filteredHabits.getPrimaryOrder(); - } - - public synchronized HabitList.Order getSecondaryOrder() - { - return filteredHabits.getSecondaryOrder(); - } - - public synchronized double getScore(long habitId) - { - return data.scores.get(habitId); - } - - public synchronized void onAttached() - { - refreshAllHabits(); - commandRunner.addListener(this); - } - - @Override - public synchronized void onCommandFinished(@Nullable Command command) - { - if (command instanceof CreateRepetitionCommand) { - Habit h = ((CreateRepetitionCommand) command).getHabit(); - Long id = h.getId(); - if (id != null) refreshHabit(id); - } else { - refreshAllHabits(); - } - } - - public synchronized void onDetached() - { - commandRunner.removeListener(this); - } - - public synchronized void refreshAllHabits() - { - if (currentFetchTask != null) currentFetchTask.cancel(); - currentFetchTask = new RefreshTask(); - taskRunner.execute(currentFetchTask); - } - - public synchronized void refreshHabit(long id) - { - taskRunner.execute(new RefreshTask(id)); - } - - public synchronized void remove(long id) - { - Habit h = data.id_to_habit.get(id); - if (h == null) return; - - int position = data.habits.indexOf(h); - data.habits.remove(position); - data.id_to_habit.remove(id); - data.checkmarks.remove(id); - data.scores.remove(id); - - listener.onItemRemoved(position); - } - - public synchronized void reorder(int from, int to) - { - Habit fromHabit = data.habits.get(from); - data.habits.remove(from); - data.habits.add(to, fromHabit); - listener.onItemMoved(from, to); - } - - public synchronized void setCheckmarkCount(int checkmarkCount) - { - this.checkmarkCount = checkmarkCount; - } - - public synchronized void setFilter(@NonNull HabitMatcher matcher) - { - if (matcher == null) throw new NullPointerException(); - filteredHabits = allHabits.getFiltered(matcher); - } - - public synchronized void setListener(@NonNull Listener listener) - { - if (listener == null) throw new NullPointerException(); - this.listener = listener; - } - - public synchronized void setPrimaryOrder(@NonNull HabitList.Order order) - { - if (order == null) throw new NullPointerException(); - allHabits.setPrimaryOrder(order); - filteredHabits.setPrimaryOrder(order); - refreshAllHabits(); - } - - public synchronized void setSecondaryOrder(@NonNull HabitList.Order order) - { - allHabits.setSecondaryOrder(order); - filteredHabits.setSecondaryOrder(order); - refreshAllHabits(); - } - - - /** - * Interface definition for a callback to be invoked when the data on the - * cache has been modified. - */ - public interface Listener - { - default void onItemChanged(int position) - { - } - - default void onItemInserted(int position) - { - } - - default void onItemMoved(int oldPosition, int newPosition) - { - } - - default void onItemRemoved(int position) - { - } - - default void onRefreshFinished() - { - } - } - - private class CacheData - { - @NonNull - public final HashMap id_to_habit; - - @NonNull - public final List habits; - - @NonNull - public final HashMap checkmarks; - - @NonNull - public final HashMap scores; - - /** - * Creates a new CacheData without any content. - */ - public CacheData() - { - id_to_habit = new HashMap<>(); - habits = new LinkedList<>(); - checkmarks = new HashMap<>(); - scores = new HashMap<>(); - } - - 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()) - { - if (oldData.checkmarks.containsKey(id)) - checkmarks.put(id, oldData.checkmarks.get(id)); - else checkmarks.put(id, empty); - } - } - - 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)) - scores.put(id, oldData.scores.get(id)); - else scores.put(id, 0.0); - } - } - - public synchronized void fetchHabits() - { - for (Habit h : filteredHabits) - { - if (h.getId() == null) continue; - habits.add(h); - id_to_habit.put(h.getId(), h); - } - } - } - - private class RefreshTask implements Task - { - @NonNull - private final CacheData newData; - - @Nullable - private final Long targetId; - - private boolean isCancelled; - - @Nullable - private TaskRunner runner; - - public RefreshTask() - { - newData = new CacheData(); - targetId = null; - isCancelled = false; - } - - public RefreshTask(long targetId) - { - newData = new CacheData(); - this.targetId = targetId; - } - - @Override - public synchronized void cancel() - { - isCancelled = true; - } - - @Override - public synchronized void doInBackground() - { - newData.fetchHabits(); - newData.copyScoresFrom(data); - newData.copyCheckmarksFrom(data); - - Timestamp today = DateUtils.getTodayWithOffset(); - Timestamp dateFrom = today.minus(checkmarkCount - 1); - - if (runner != null) runner.publishProgress(this, -1); - - for (int position = 0; position < newData.habits.size(); position++) - { - if (isCancelled) return; - - Habit habit = newData.habits.get(position); - Long id = habit.getId(); - if (targetId != null && !targetId.equals(id)) continue; - - newData.scores.put(id, habit.getScores().get(today).getValue()); - Integer[] entries = habit.getComputedEntries() - .getByInterval(dateFrom, today) - .stream() - .map(Entry::getValue) - .toArray(Integer[]::new); - newData.checkmarks.put(id, ArrayUtils.toPrimitive(entries)); - - runner.publishProgress(this, position); - } - } - - @Override - public synchronized void onAttached(@NonNull TaskRunner runner) - { - if (runner == null) throw new NullPointerException(); - this.runner = runner; - } - - @Override - public synchronized void onPostExecute() - { - currentFetchTask = null; - listener.onRefreshFinished(); - } - - @Override - public synchronized void onProgressUpdate(int currentPosition) - { - if (currentPosition < 0) processRemovedHabits(); - else processPosition(currentPosition); - } - - private synchronized void performInsert(Habit habit, int position) - { - Long id = habit.getId(); - data.habits.add(position, habit); - data.id_to_habit.put(id, habit); - data.scores.put(id, newData.scores.get(id)); - data.checkmarks.put(id, newData.checkmarks.get(id)); - listener.onItemInserted(position); - } - - 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 synchronized void performUpdate(long id, int position) - { - double oldScore = data.scores.get(id); - int[] oldCheckmarks = data.checkmarks.get(id); - - double newScore = newData.scores.get(id); - int[] newCheckmarks = newData.checkmarks.get(id); - - boolean unchanged = true; - if (oldScore != newScore) unchanged = false; - if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false; - if (unchanged) return; - - data.scores.put(id, newScore); - data.checkmarks.put(id, newCheckmarks); - listener.onItemChanged(position); - } - - private synchronized void processPosition(int currentPosition) - { - Habit habit = newData.habits.get(currentPosition); - Long id = habit.getId(); - - int prevPosition = data.habits.indexOf(habit); - - if (prevPosition < 0) - { - performInsert(habit, currentPosition); - } - else - { - if (prevPosition != currentPosition) - performMove(habit, prevPosition, currentPosition); - - performUpdate(id, currentPosition); - } - } - - private synchronized void processRemovedHabits() - { - Set before = data.id_to_habit.keySet(); - Set after = newData.id_to_habit.keySet(); - - Set removed = new TreeSet<>(before); - removed.removeAll(after); - - for (Long id : removed) remove(id); - } - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt new file mode 100644 index 000000000..2f0fdf17e --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui.screens.habits.list + +import org.apache.commons.lang3.ArrayUtils +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.core.commands.Command +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitList.Order +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.tasks.Task +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.ArrayList +import java.util.Arrays +import java.util.HashMap +import java.util.LinkedList +import java.util.TreeSet +import javax.inject.Inject + +/** + * A HabitCardListCache fetches and keeps a cache of all the data necessary to + * render a HabitCardListView. + * + * + * This is needed since performing database lookups during scrolling can make + * the ListView very slow. It also registers itself as an observer of the + * models, in order to update itself automatically. + * + * + * Note that this class is singleton-scoped, therefore it is shared among all + * activities. + */ +@AppScope +class HabitCardListCache @Inject constructor( + private val allHabits: HabitList, + private val commandRunner: CommandRunner, + taskRunner: TaskRunner +) : CommandRunner.Listener { + private var checkmarkCount = 0 + private var currentFetchTask: Task? = null + private var listener: Listener + private val data: CacheData + private var filteredHabits: HabitList + private val taskRunner: TaskRunner + @Synchronized + fun cancelTasks() { + currentFetchTask?.cancel() + } + + @Synchronized + fun getCheckmarks(habitId: Long): IntArray { + return data.checkmarks[habitId]!! + } + + /** + * Returns the habits that occupies a certain position on the list. + * + * @param position the position of the habit + * @return the habit at given position or null if position is invalid + */ + @Synchronized + fun getHabitByPosition(position: Int): Habit? { + return if (position < 0 || position >= data.habits.size) null else data.habits[position] + } + + @get:Synchronized + val habitCount: Int + get() = data.habits.size + + @get:Synchronized + @set:Synchronized + var primaryOrder: Order + get() = filteredHabits.primaryOrder + set(order) { + allHabits.primaryOrder = order + filteredHabits.primaryOrder = order + refreshAllHabits() + } + + @get:Synchronized + @set:Synchronized + var secondaryOrder: Order + get() = filteredHabits.secondaryOrder + set(order) { + allHabits.secondaryOrder = order + filteredHabits.secondaryOrder = order + refreshAllHabits() + } + + @Synchronized + fun getScore(habitId: Long): Double { + return data.scores[habitId]!! + } + + @Synchronized + fun onAttached() { + refreshAllHabits() + commandRunner.addListener(this) + } + + @Synchronized + override fun onCommandFinished(command: Command) { + if (command is CreateRepetitionCommand) { + val (_, _, _, id) = command.habit + id?.let { refreshHabit(it) } + } else { + refreshAllHabits() + } + } + + @Synchronized + fun onDetached() { + commandRunner.removeListener(this) + } + + @Synchronized + fun refreshAllHabits() { + if (currentFetchTask != null) currentFetchTask!!.cancel() + currentFetchTask = RefreshTask() + taskRunner.execute(currentFetchTask) + } + + @Synchronized + fun refreshHabit(id: Long) { + taskRunner.execute(RefreshTask(id)) + } + + @Synchronized + fun remove(id: Long) { + val h = data.idToHabit[id] ?: return + val position = data.habits.indexOf(h) + data.habits.removeAt(position) + data.idToHabit.remove(id) + data.checkmarks.remove(id) + data.scores.remove(id) + listener.onItemRemoved(position) + } + + @Synchronized + fun reorder(from: Int, to: Int) { + val fromHabit = data.habits[from] + data.habits.removeAt(from) + data.habits.add(to, fromHabit) + listener.onItemMoved(from, to) + } + + @Synchronized + fun setCheckmarkCount(checkmarkCount: Int) { + this.checkmarkCount = checkmarkCount + } + + @Synchronized + fun setFilter(matcher: HabitMatcher) { + filteredHabits = allHabits.getFiltered(matcher) + } + + @Synchronized + fun setListener(listener: Listener) { + this.listener = listener + } + + /** + * Interface definition for a callback to be invoked when the data on the + * cache has been modified. + */ + interface Listener { + fun onItemChanged(position: Int) {} + fun onItemInserted(position: Int) {} + fun onItemMoved(oldPosition: Int, newPosition: Int) {} + fun onItemRemoved(position: Int) {} + fun onRefreshFinished() {} + } + + private inner class CacheData { + val idToHabit: HashMap = HashMap() + val habits: MutableList + val checkmarks: HashMap + val scores: HashMap + @Synchronized + fun copyCheckmarksFrom(oldData: CacheData) { + val empty = IntArray(checkmarkCount) + for (id in idToHabit.keys) { + if (oldData.checkmarks.containsKey(id)) checkmarks[id] = + oldData.checkmarks[id]!! else checkmarks[id] = empty + } + } + + @Synchronized + fun copyScoresFrom(oldData: CacheData) { + for (id in idToHabit.keys) { + if (oldData.scores.containsKey(id)) scores[id] = + oldData.scores[id]!! else scores[id] = 0.0 + } + } + + @Synchronized + fun fetchHabits() { + for (h in filteredHabits) { + if (h.id == null) continue + habits.add(h) + idToHabit[h.id] = h + } + } + + /** + * Creates a new CacheData without any content. + */ + init { + habits = LinkedList() + checkmarks = HashMap() + scores = HashMap() + } + } + + private inner class RefreshTask : Task { + private val newData: CacheData + private val targetId: Long? + private var isCancelled = false + private var runner: TaskRunner? = null + + constructor() { + newData = CacheData() + targetId = null + isCancelled = false + } + + constructor(targetId: Long) { + newData = CacheData() + this.targetId = targetId + } + + @Synchronized + override fun cancel() { + isCancelled = true + } + + @Synchronized + override fun doInBackground() { + newData.fetchHabits() + newData.copyScoresFrom(data) + newData.copyCheckmarksFrom(data) + val today = getTodayWithOffset() + val dateFrom = today.minus(checkmarkCount - 1) + if (runner != null) runner!!.publishProgress(this, -1) + for (position in newData.habits.indices) { + if (isCancelled) return + val (_, _, _, id, _, _, _, _, _, _, _, _, _, _, computedEntries, _, scores) = newData.habits[position] + if (targetId != null && targetId != id) continue + newData.scores[id] = scores[today].value + val list: MutableList = ArrayList() + for ( + (_, value) in computedEntries + .getByInterval(dateFrom, today) + ) { + list.add(value) + } + val entries = list.toTypedArray() + newData.checkmarks[id] = ArrayUtils.toPrimitive(entries) + runner!!.publishProgress(this, position) + } + } + + @Synchronized + override fun onAttached(runner: TaskRunner) { + this.runner = runner + } + + @Synchronized + override fun onPostExecute() { + currentFetchTask = null + listener.onRefreshFinished() + } + + @Synchronized + override fun onProgressUpdate(currentPosition: Int) { + if (currentPosition < 0) processRemovedHabits() else processPosition(currentPosition) + } + + @Synchronized + private fun performInsert(habit: Habit, position: Int) { + val id = habit.id + data.habits.add(position, habit) + data.idToHabit[id] = habit + data.scores[id] = newData.scores[id]!! + data.checkmarks[id] = newData.checkmarks[id]!! + listener.onItemInserted(position) + } + + @Synchronized + private fun performMove( + habit: Habit, + fromPosition: Int, + toPosition: Int + ) { + data.habits.removeAt(fromPosition) + data.habits.add(toPosition, habit) + listener.onItemMoved(fromPosition, toPosition) + } + + @Synchronized + private fun performUpdate(id: Long, position: Int) { + val oldScore = data.scores[id]!! + val oldCheckmarks = data.checkmarks[id] + val newScore = newData.scores[id]!! + val newCheckmarks = newData.checkmarks[id]!! + var unchanged = true + if (oldScore != newScore) unchanged = false + if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false + if (unchanged) return + data.scores[id] = newScore + data.checkmarks[id] = newCheckmarks + listener.onItemChanged(position) + } + + @Synchronized + private fun processPosition(currentPosition: Int) { + val habit = newData.habits[currentPosition] + val id = habit.id + val prevPosition = data.habits.indexOf(habit) + if (prevPosition < 0) { + performInsert(habit, currentPosition) + } else { + if (prevPosition != currentPosition) performMove( + habit, + prevPosition, + currentPosition + ) + if (id == null) throw NullPointerException() + performUpdate(id, currentPosition) + } + } + + @Synchronized + private fun processRemovedHabits() { + val before: Set = data.idToHabit.keys + val after: Set = newData.idToHabit.keys + val removed: MutableSet = TreeSet(before) + removed.removeAll(after) + for (id in removed) remove(id!!) + } + } + + init { + filteredHabits = allHabits + this.taskRunner = taskRunner + listener = object : Listener {} + data = CacheData() + } +} From 136ec5b49b6fb2108e796d481114377bd5a2fe3c Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Tue, 19 Jan 2021 23:24:22 +0100 Subject: [PATCH 06/35] Convert most of code remaining in uhabits-core --- .../habits/list/ListHabitsScreen.kt | 7 +- .../habits/list/views/HabitCardListAdapter.kt | 42 ++-- .../widgets/views/CheckmarkWidgetView.kt | 8 +- .../uhabits/core/io/HabitsCSVExporter.kt | 2 +- .../isoron/uhabits/core/models/HabitList.java | 2 +- .../core/models/memory/MemoryHabitList.java | 7 +- .../core/models/sqlite/SQLiteHabitList.java | 2 + .../models/sqlite/records/HabitRecord.java | 4 +- .../core/preferences/WidgetPreferences.java | 3 +- .../core/reminders/ReminderScheduler.java | 2 +- .../uhabits/core/ui/NotificationTray.java | 4 +- .../screens/habits/list/HabitCardListCache.kt | 3 +- .../list/{HintList.java => HintList.kt} | 54 ++--- .../habits/list/ListHabitsBehavior.java | 216 ------------------ .../screens/habits/list/ListHabitsBehavior.kt | 161 +++++++++++++ .../habits/list/ListHabitsMenuBehavior.java | 177 -------------- .../habits/list/ListHabitsMenuBehavior.kt | 133 +++++++++++ .../list/ListHabitsSelectionMenuBehavior.java | 145 ------------ .../list/ListHabitsSelectionMenuBehavior.kt | 116 ++++++++++ .../habits/list/ListHabitsBehaviorTest.kt | 12 +- .../habits/list/ListHabitsMenuBehaviorTest.kt | 5 +- 21 files changed, 476 insertions(+), 629 deletions(-) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/{HintList.java => HintList.kt} (57%) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index 3e552beff..e2e265aaf 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -215,7 +215,7 @@ class ListHabitsScreen ) } - override fun showSendBugReportToDeveloperScreen(log: String) { + override fun showSendBugReportToDeveloperScreen(log: String?) { val to = R.string.bugReportTo val subject = R.string.bugReportSubject activity.showSendEmailScreen(to, subject, log) @@ -230,10 +230,7 @@ class ListHabitsScreen activity.startActivityForResult(intent, REQUEST_SETTINGS) } - override fun showColorPicker( - defaultColor: PaletteColor, - callback: OnColorPickedCallback - ) { + override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) { val picker = colorPickerFactory.create(defaultColor) picker.setListener(callback) picker.show(activity.supportFragmentManager, "picker") diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 10eeb4d5b..c968a98db 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -53,7 +53,7 @@ class HabitCardListAdapter @Inject constructor( ListHabitsSelectionMenuBehavior.Adapter { val observable: ModelObservable = ModelObservable() private var listView: HabitCardListView? = null - private val selected: LinkedList = LinkedList() + override val selected: LinkedList = LinkedList() override fun atMidnight() { cache.refreshAllHabits() } @@ -90,9 +90,9 @@ class HabitCardListAdapter @Inject constructor( return getItem(position)!!.id!! } - override fun getSelected(): List { - return LinkedList(selected) - } + // override fun getSelected(): List { + // return LinkedList(selected) + // } /** * Returns whether list of selected items is empty. @@ -182,10 +182,10 @@ class HabitCardListAdapter @Inject constructor( * database operation to finish, the cache can be modified to reflect the * changes immediately. * - * @param habits list of habits to be removed + * @param selected list of habits to be removed */ - override fun performRemove(habits: List) { - for (habit in habits) cache.remove(habit.id!!) + override fun performRemove(selected: List) { + for (habit in selected) cache.remove(habit.id!!) } /** @@ -209,8 +209,8 @@ class HabitCardListAdapter @Inject constructor( cache.refreshAllHabits() } - override fun setFilter(matcher: HabitMatcher) { - cache.setFilter(matcher) + override fun setFilter(matcher: HabitMatcher?) { + if (matcher != null) cache.setFilter(matcher) } /** @@ -225,19 +225,19 @@ class HabitCardListAdapter @Inject constructor( this.listView = listView } - override fun setPrimaryOrder(order: HabitList.Order) { - cache.primaryOrder = order - preferences.defaultPrimaryOrder = order - } + override var primaryOrder: HabitList.Order + get() = cache.primaryOrder + set(value) { + cache.primaryOrder = value + preferences.defaultPrimaryOrder = value + } - override fun setSecondaryOrder(order: HabitList.Order) { - cache.secondaryOrder = order - preferences.defaultSecondaryOrder = order - } - - override fun getPrimaryOrder(): HabitList.Order { - return cache.primaryOrder - } + override var secondaryOrder: HabitList.Order + get() = cache.secondaryOrder + set(value) { + cache.secondaryOrder = value + preferences.defaultSecondaryOrder = value + } /** * Selects or deselects the item at a given position. diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt index a74c7982f..e30dcc22a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt @@ -117,8 +117,6 @@ class CheckmarkWidgetView : HabitWidgetView { get() = R.layout.widget_checkmark override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - var widthMeasureSpec = widthMeasureSpec - var heightMeasureSpec = heightMeasureSpec val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) var w = width.toFloat() @@ -128,15 +126,15 @@ class CheckmarkWidgetView : HabitWidgetView { h *= scale if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility = GONE else ring.visibility = VISIBLE - widthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY) - heightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY) + val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY) + val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY) var textSize = 0.15f * h val maxTextSize = getDimension(context, R.dimen.smallerTextSize) textSize = min(textSize, maxTextSize) label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) ring.setTextSize(textSize) ring.setThickness(0.15f * textSize) - super.onMeasure(widthMeasureSpec, heightMeasureSpec) + super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec) } private fun init() { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt index 3126b18a3..341b6716c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt @@ -77,7 +77,7 @@ class HabitsCSVExporter( } private fun sanitizeFilename(name: String): String { - val s = name.replace("[^ a-zA-Z0-9\\._-]+".toRegex(), "") + val s = name.replace("[^ a-zA-Z0-9._-]+".toRegex(), "") return s.substring(0, min(s.length, 100)) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java index 67f0eef74..2ca00c56b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java @@ -219,7 +219,7 @@ public abstract class HabitList implements Iterable */ public void writeCSV(@NonNull Writer out) throws IOException { - String header[] = { + String[] header = { "Position", "Name", "Question", diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java index 17e0b092b..b755c7588 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java @@ -23,6 +23,7 @@ import androidx.annotation.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.utils.*; +import org.jetbrains.annotations.NotNull; import java.util.*; @@ -96,7 +97,7 @@ public class MemoryHabitList extends HabitList @Override public synchronized Habit getByUUID(String uuid) { - for (Habit h : list) if (h.getUuid().equals(uuid)) return h; + for (Habit h : list) if (Objects.requireNonNull(h.getUuid()).equals(uuid)) return h; return null; } @@ -114,12 +115,14 @@ public class MemoryHabitList extends HabitList return new MemoryHabitList(matcher, comparator, this); } + @NotNull @Override public synchronized Order getPrimaryOrder() { return primaryOrder; } + @NotNull @Override public synchronized Order getSecondaryOrder() { @@ -288,7 +291,7 @@ public class MemoryHabitList extends HabitList public synchronized void resort() { - if (comparator != null) Collections.sort(list, comparator); + if (comparator != null) list.sort(comparator); getObservable().notifyListeners(); } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java index 77c3a5923..4e8426a7d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java @@ -25,6 +25,7 @@ import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.memory.*; import org.isoron.uhabits.core.models.sqlite.records.*; +import org.jetbrains.annotations.NotNull; import java.util.*; @@ -161,6 +162,7 @@ public class SQLiteHabitList extends HabitList return list.indexOf(h); } + @NotNull @Override public synchronized Iterator iterator() { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java index f7584702c..8ddaeb183 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java @@ -22,6 +22,8 @@ package org.isoron.uhabits.core.models.sqlite.records; import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.models.*; +import java.util.Objects; + /** * The SQLite database record corresponding to a {@link Habit}. */ @@ -108,7 +110,7 @@ public class HabitRecord if (model.hasReminder()) { Reminder reminder = model.getReminder(); - this.reminderHour = reminder.getHour(); + this.reminderHour = Objects.requireNonNull(reminder).getHour(); this.reminderMin = reminder.getMinute(); this.reminderDays = reminder.getDays().toInteger(); } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java index 838b1aecc..b75316abe 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java @@ -20,7 +20,6 @@ package org.isoron.uhabits.core.preferences; import org.isoron.uhabits.core.AppScope; -import org.isoron.uhabits.core.models.HabitNotFoundException; import javax.inject.Inject; @@ -33,7 +32,7 @@ public class WidgetPreferences { this.storage = storage; } - public void addWidget(int widgetId, long habitIds[]) { + public void addWidget(int widgetId, long[] habitIds) { storage.putLongArray(getHabitIdKey(widgetId), habitIds); } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java index b9d360eb9..5f93c32c7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java @@ -77,7 +77,7 @@ public class ReminderScheduler implements CommandRunner.Listener return; } - long reminderTime = habit.getReminder().getTimeInMillis(); + long reminderTime = Objects.requireNonNull(habit.getReminder()).getTimeInMillis(); long snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.getId()); if (snoozeReminderTime != 0) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java index cfb77a152..74c654794 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java @@ -145,7 +145,7 @@ public class NotificationTray void log(String msg); } - class NotificationData + static class NotificationData { public final Timestamp timestamp; @@ -229,7 +229,7 @@ public class NotificationTray if (!habit.hasReminder()) return false; Reminder reminder = habit.getReminder(); - boolean reminderDays[] = reminder.getDays().toArray(); + boolean[] reminderDays = Objects.requireNonNull(reminder).getDays().toArray(); int weekday = timestamp.getWeekday(); return reminderDays[weekday]; diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 2f0fdf17e..cb3fc9748 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -121,8 +121,7 @@ class HabitCardListCache @Inject constructor( @Synchronized override fun onCommandFinished(command: Command) { if (command is CreateRepetitionCommand) { - val (_, _, _, id) = command.habit - id?.let { refreshHabit(it) } + command.habit.id?.let { refreshHabit(it) } } else { refreshAllHabits() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt similarity index 57% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt index 3f77041bb..8ef33a051 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt @@ -16,54 +16,31 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.ui.screens.habits.list -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.utils.*; +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday /** * Provides a list of hints to be shown at the application startup, and takes * care of deciding when a new hint should be shown. */ -public class HintList -{ - private final Preferences prefs; - - @NonNull - private final String[] hints; - - /** - * Constructs a new list containing the provided hints. - * - * @param hints initial list of hints - */ - public HintList(@NonNull Preferences prefs, - @NonNull String hints[]) - { - this.prefs = prefs; - this.hints = hints; - } - +class HintList(private val prefs: Preferences, private val hints: Array) { /** * Returns a new hint to be shown to the user. - *

+ * + * * The hint returned is marked as read on the list, and will not be returned * again. In case all hints have already been read, and there is nothing * left, returns null. * * @return the next hint to be shown, or null if none */ - public String pop() - { - int next = prefs.getLastHintNumber() + 1; - if (next >= hints.length) return null; - - prefs.updateLastHint(next, DateUtils.getToday()); - return hints[next]; + fun pop(): String? { + val next = prefs.lastHintNumber + 1 + if (next >= hints.size) return null + prefs.updateLastHint(next, getToday()) + return hints[next] } /** @@ -71,10 +48,9 @@ public class HintList * * @return true if hint should be shown, false otherwise */ - public boolean shouldShow() - { - Timestamp today = DateUtils.getToday(); - Timestamp lastHintTimestamp = prefs.getLastHintTimestamp(); - return (lastHintTimestamp.isOlderThan(today)); + fun shouldShow(): Boolean { + val today = getToday() + val lastHintTimestamp = prefs.lastHintTimestamp + return lastHintTimestamp.isOlderThan(today) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java deleted file mode 100644 index 53abce813..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.ui.callbacks.*; -import org.isoron.uhabits.core.utils.*; -import org.jetbrains.annotations.*; - -import java.io.*; -import java.util.*; - -import javax.inject.*; - -public class ListHabitsBehavior -{ - @NonNull - private final HabitList habitList; - - @NonNull - private final DirFinder dirFinder; - - @NonNull - private final TaskRunner taskRunner; - - @NonNull - private final Screen screen; - - @NonNull - private final CommandRunner commandRunner; - - @NonNull - private final Preferences prefs; - - @NonNull - private final BugReporter bugReporter; - - @Inject - public ListHabitsBehavior(@NonNull HabitList habitList, - @NonNull DirFinder dirFinder, - @NonNull TaskRunner taskRunner, - @NonNull Screen screen, - @NonNull CommandRunner commandRunner, - @NonNull Preferences prefs, - @NonNull BugReporter bugReporter) - { - this.habitList = habitList; - this.dirFinder = dirFinder; - this.taskRunner = taskRunner; - this.screen = screen; - this.commandRunner = commandRunner; - this.prefs = prefs; - this.bugReporter = bugReporter; - } - - public void onClickHabit(@NonNull Habit h) - { - screen.showHabitScreen(h); - } - - public void onEdit(@NonNull Habit habit, Timestamp timestamp) - { - EntryList entries = habit.getComputedEntries(); - double oldValue = entries.get(timestamp).getValue(); - - screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue -> - { - newValue = Math.round(newValue * 1000); - commandRunner.run( - new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue) - ); - }); - } - - public void onExportCSV() - { - List selected = new LinkedList<>(); - for (Habit h : habitList) selected.add(h); - File outputDir = dirFinder.getCSVOutputDir(); - - taskRunner.execute( - new ExportCSVTask(habitList, selected, outputDir, filename -> - { - if (filename != null) screen.showSendFileScreen(filename); - else screen.showMessage(Message.COULD_NOT_EXPORT); - })); - } - - public void onFirstRun() - { - prefs.setFirstRun(false); - prefs.updateLastHint(-1, DateUtils.getToday()); - screen.showIntroScreen(); - } - - public void onReorderHabit(@NonNull Habit from, @NonNull Habit to) - { - taskRunner.execute(() -> habitList.reorder(from, to)); - } - - public void onRepairDB() - { - taskRunner.execute(() -> - { - habitList.repair(); - screen.showMessage(Message.DATABASE_REPAIRED); - }); - } - - public void onSendBugReport() - { - bugReporter.dumpBugReportToFile(); - - try - { - String log = bugReporter.getBugReport(); - screen.showSendBugReportToDeveloperScreen(log); - } - catch (IOException e) - { - e.printStackTrace(); - screen.showMessage(Message.COULD_NOT_GENERATE_BUG_REPORT); - } - } - - public void onStartup() - { - prefs.incrementLaunchCount(); - if (prefs.isFirstRun()) onFirstRun(); - } - - public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value) - { - commandRunner.run( - new CreateRepetitionCommand(habitList, habit, timestamp, value) - ); - } - - public void onSyncKeyOffer(@NotNull String syncKey, @NotNull String encryptionKey) - { - if(prefs.getSyncKey().equals(syncKey)) { - screen.showMessage(Message.SYNC_KEY_ALREADY_INSTALLED); - return; - } - screen.showConfirmInstallSyncKey(() -> { - prefs.enableSync(syncKey, encryptionKey); - screen.showMessage(Message.SYNC_ENABLED); - }); - } - - public enum Message - { - COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, - COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED, SYNC_KEY_ALREADY_INSTALLED - } - - public interface BugReporter - { - void dumpBugReportToFile(); - - String getBugReport() throws IOException; - } - - public interface DirFinder - { - File getCSVOutputDir(); - } - - public interface NumberPickerCallback - { - void onNumberPicked(double newValue); - - default void onNumberPickerDismissed() {} - } - - public interface Screen - { - void showHabitScreen(@NonNull Habit h); - - void showIntroScreen(); - - void showMessage(@NonNull Message m); - - void showNumberPicker(double value, - @NonNull String unit, - @NonNull NumberPickerCallback callback); - - void showSendBugReportToDeveloperScreen(String log); - - void showSendFileScreen(@NonNull String filename); - - void showConfirmInstallSyncKey(@NonNull OnConfirmedCallback callback); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt new file mode 100644 index 000000000..8144a8fb3 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui.screens.habits.list + +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.tasks.ExportCSVTask +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import java.io.File +import java.io.IOException +import java.util.LinkedList +import javax.inject.Inject + +class ListHabitsBehavior @Inject constructor( + private val habitList: HabitList, + private val dirFinder: DirFinder, + private val taskRunner: TaskRunner, + private val screen: Screen, + private val commandRunner: CommandRunner, + private val prefs: Preferences, + private val bugReporter: BugReporter +) { + fun onClickHabit(h: Habit) { + screen.showHabitScreen(h) + } + + fun onEdit(habit: Habit, timestamp: Timestamp?) { + val entries = habit.computedEntries + val oldValue = entries.get(timestamp!!).value.toDouble() + screen.showNumberPicker( + oldValue / 1000, + habit.unit, + { newValue: Double -> + val value = Math.round(newValue * 1000).toDouble() + commandRunner.run( + CreateRepetitionCommand(habitList, habit, timestamp, value.toInt()) + ) + } + ) + } + + fun onExportCSV() { + val selected: MutableList = LinkedList() + for (h in habitList) selected.add(h) + val outputDir = dirFinder.getCSVOutputDir() + taskRunner.execute( + ExportCSVTask(habitList, selected, outputDir) { filename: String? -> + if (filename != null) screen.showSendFileScreen(filename) else screen.showMessage( + Message.COULD_NOT_EXPORT + ) + } + ) + } + + fun onFirstRun() { + prefs.isFirstRun = false + prefs.updateLastHint(-1, getToday()) + screen.showIntroScreen() + } + + fun onReorderHabit(from: Habit, to: Habit) { + taskRunner.execute { habitList.reorder(from, to) } + } + + fun onRepairDB() { + taskRunner.execute { + habitList.repair() + screen.showMessage(Message.DATABASE_REPAIRED) + } + } + + fun onSendBugReport() { + bugReporter.dumpBugReportToFile() + try { + val log = bugReporter.getBugReport() + screen.showSendBugReportToDeveloperScreen(log) + } catch (e: IOException) { + e.printStackTrace() + screen.showMessage(Message.COULD_NOT_GENERATE_BUG_REPORT) + } + } + + fun onStartup() { + prefs.incrementLaunchCount() + if (prefs.isFirstRun) onFirstRun() + } + + fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) { + commandRunner.run( + CreateRepetitionCommand(habitList, habit, timestamp!!, value) + ) + } + + fun onSyncKeyOffer(syncKey: String, encryptionKey: String) { + if (prefs.syncKey == syncKey) { + screen.showMessage(Message.SYNC_KEY_ALREADY_INSTALLED) + return + } + screen.showConfirmInstallSyncKey { + prefs.enableSync(syncKey, encryptionKey) + screen.showMessage(Message.SYNC_ENABLED) + } + } + + enum class Message { + COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED, SYNC_KEY_ALREADY_INSTALLED + } + + interface BugReporter { + fun dumpBugReportToFile() + + @Throws(IOException::class) + fun getBugReport(): String + } + + interface DirFinder { + fun getCSVOutputDir(): File + } + + fun interface NumberPickerCallback { + fun onNumberPicked(newValue: Double) + fun onNumberPickerDismissed() {} + } + + interface Screen { + fun showHabitScreen(h: Habit) + fun showIntroScreen() + fun showMessage(m: Message) + fun showNumberPicker( + value: Double, + unit: String, + callback: NumberPickerCallback + ) + + fun showSendBugReportToDeveloperScreen(log: String?) + fun showSendFileScreen(filename: String) + fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.java deleted file mode 100644 index 97e801bce..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.ui.*; - -import javax.inject.*; - -public class ListHabitsMenuBehavior -{ - @NonNull - private final Screen screen; - - @NonNull - private final Adapter adapter; - - @NonNull - private final Preferences preferences; - - @NonNull - private final ThemeSwitcher themeSwitcher; - - private boolean showCompleted; - - private boolean showArchived; - - @Inject - public ListHabitsMenuBehavior(@NonNull Screen screen, - @NonNull Adapter adapter, - @NonNull Preferences preferences, - @NonNull ThemeSwitcher themeSwitcher) - { - this.screen = screen; - this.adapter = adapter; - this.preferences = preferences; - this.themeSwitcher = themeSwitcher; - - showCompleted = preferences.getShowCompleted(); - showArchived = preferences.getShowArchived(); - updateAdapterFilter(); - } - - public void onCreateHabit() - { - screen.showSelectHabitTypeDialog(); - } - - public void onViewFAQ() - { - screen.showFAQScreen(); - } - - public void onViewAbout() - { - screen.showAboutScreen(); - } - - public void onViewSettings() - { - screen.showSettingsScreen(); - } - - public void onToggleShowArchived() - { - showArchived = !showArchived; - preferences.setShowArchived(showArchived); - updateAdapterFilter(); - } - - public void onToggleShowCompleted() - { - showCompleted = !showCompleted; - preferences.setShowCompleted(showCompleted); - updateAdapterFilter(); - } - - public void onSortByManually() - { - adapter.setPrimaryOrder(HabitList.Order.BY_POSITION); - } - - public void onSortByColor() - { - onSortToggleBy(HabitList.Order.BY_COLOR_ASC, HabitList.Order.BY_COLOR_DESC); - } - - - public void onSortByScore() - { - onSortToggleBy(HabitList.Order.BY_SCORE_DESC, HabitList.Order.BY_SCORE_ASC); - } - - public void onSortByName() - { - onSortToggleBy(HabitList.Order.BY_NAME_ASC, HabitList.Order.BY_NAME_DESC); - } - - public void onSortByStatus() - { - onSortToggleBy(HabitList.Order.BY_STATUS_ASC, HabitList.Order.BY_STATUS_DESC); - } - - private void onSortToggleBy(HabitList.Order defaultOrder, HabitList.Order reversedOrder) - { - if (adapter.getPrimaryOrder() != defaultOrder) { - if (adapter.getPrimaryOrder() != reversedOrder) { - adapter.setSecondaryOrder(adapter.getPrimaryOrder()); - } - adapter.setPrimaryOrder(defaultOrder); - } else { - adapter.setPrimaryOrder(reversedOrder); - } - - } - - public void onToggleNightMode() - { - themeSwitcher.toggleNightMode(); - screen.applyTheme(); - } - - private void updateAdapterFilter() - { - adapter.setFilter(new HabitMatcherBuilder() - .setArchivedAllowed(showArchived) - .setCompletedAllowed(showCompleted) - .build()); - adapter.refresh(); - } - - public interface Adapter - { - void refresh(); - - void setFilter(HabitMatcher build); - - void setPrimaryOrder(HabitList.Order order); - - void setSecondaryOrder(HabitList.Order order); - - HabitList.Order getPrimaryOrder(); - } - - public interface Screen - { - void applyTheme(); - - void showAboutScreen(); - - void showFAQScreen(); - - void showSettingsScreen(); - - void showSelectHabitTypeDialog(); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt new file mode 100644 index 000000000..268ce4e94 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui.screens.habits.list + +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.models.HabitMatcherBuilder +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.ThemeSwitcher +import javax.inject.Inject + +class ListHabitsMenuBehavior @Inject constructor( + private val screen: Screen, + private val adapter: Adapter, + private val preferences: Preferences, + private val themeSwitcher: ThemeSwitcher +) { + private var showCompleted: Boolean + private var showArchived: Boolean + fun onCreateHabit() { + screen.showSelectHabitTypeDialog() + } + + fun onViewFAQ() { + screen.showFAQScreen() + } + + fun onViewAbout() { + screen.showAboutScreen() + } + + fun onViewSettings() { + screen.showSettingsScreen() + } + + fun onToggleShowArchived() { + showArchived = !showArchived + preferences.showArchived = showArchived + updateAdapterFilter() + } + + fun onToggleShowCompleted() { + showCompleted = !showCompleted + preferences.showCompleted = showCompleted + updateAdapterFilter() + } + + fun onSortByManually() { + adapter.primaryOrder = HabitList.Order.BY_POSITION + } + + fun onSortByColor() { + onSortToggleBy(HabitList.Order.BY_COLOR_ASC, HabitList.Order.BY_COLOR_DESC) + } + + fun onSortByScore() { + onSortToggleBy(HabitList.Order.BY_SCORE_DESC, HabitList.Order.BY_SCORE_ASC) + } + + fun onSortByName() { + onSortToggleBy(HabitList.Order.BY_NAME_ASC, HabitList.Order.BY_NAME_DESC) + } + + fun onSortByStatus() { + onSortToggleBy(HabitList.Order.BY_STATUS_ASC, HabitList.Order.BY_STATUS_DESC) + } + + private fun onSortToggleBy(defaultOrder: HabitList.Order, reversedOrder: HabitList.Order) { + if (adapter.primaryOrder != defaultOrder) { + if (adapter.primaryOrder != reversedOrder) { + adapter.secondaryOrder = adapter.primaryOrder + } + adapter.primaryOrder = defaultOrder + } else { + adapter.primaryOrder = reversedOrder + } + } + + fun onToggleNightMode() { + themeSwitcher.toggleNightMode() + screen.applyTheme() + } + + private fun updateAdapterFilter() { + adapter.setFilter( + HabitMatcherBuilder() + .setArchivedAllowed(showArchived) + .setCompletedAllowed(showCompleted) + .build() + ) + adapter.refresh() + } + + interface Adapter { + fun refresh() + fun setFilter(matcher: HabitMatcher?) + // fun setSecondaryOrder(order: HabitList.Order) + // fun setPrimaryOrder(order: HabitList.Order) + // fun getPrimaryOrder(): HabitList.Order + var primaryOrder: HabitList.Order + var secondaryOrder: HabitList.Order + } + + interface Screen { + fun applyTheme() + fun showAboutScreen() + fun showFAQScreen() + fun showSettingsScreen() + fun showSelectHabitTypeDialog() + } + + init { + showCompleted = preferences.showCompleted + showArchived = preferences.showArchived + updateAdapterFilter() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.java deleted file mode 100644 index d0cc197d0..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.ui.callbacks.*; - -import java.util.*; - -import javax.inject.*; - -public class ListHabitsSelectionMenuBehavior -{ - @NonNull - private final Screen screen; - - @NonNull - CommandRunner commandRunner; - - @NonNull - private final Adapter adapter; - - @NonNull - private final HabitList habitList; - - @Inject - public ListHabitsSelectionMenuBehavior(@NonNull HabitList habitList, - @NonNull Screen screen, - @NonNull Adapter adapter, - @NonNull CommandRunner commandRunner) - { - this.habitList = habitList; - this.screen = screen; - this.adapter = adapter; - this.commandRunner = commandRunner; - } - - public boolean canArchive() - { - for (Habit h : adapter.getSelected()) - if (h.isArchived()) return false; - - return true; - } - - public boolean canEdit() - { - return (adapter.getSelected().size() == 1); - } - - public boolean canUnarchive() - { - for (Habit h : adapter.getSelected()) - if (!h.isArchived()) return false; - - return true; - } - - public void onArchiveHabits() - { - commandRunner.run( - new ArchiveHabitsCommand(habitList, adapter.getSelected())); - adapter.clearSelection(); - } - - public void onChangeColor() - { - List selected = adapter.getSelected(); - Habit first = selected.get(0); - - screen.showColorPicker(first.getColor(), selectedColor -> - { - commandRunner.run( - new ChangeHabitColorCommand(habitList, selected, selectedColor) - ); - adapter.clearSelection(); - }); - } - - public void onDeleteHabits() - { - List selected = adapter.getSelected(); - screen.showDeleteConfirmationScreen(() -> - { - adapter.performRemove(selected); - commandRunner.run(new DeleteHabitsCommand(habitList, selected) - ); - adapter.clearSelection(); - }, selected.size()); - } - - public void onEditHabits() - { - screen.showEditHabitsScreen(adapter.getSelected()); - adapter.clearSelection(); - } - - public void onUnarchiveHabits() - { - commandRunner.run( - new UnarchiveHabitsCommand(habitList, adapter.getSelected())); - adapter.clearSelection(); - } - - public interface Adapter - { - void clearSelection(); - - List getSelected(); - - void performRemove(List selected); - } - - public interface Screen - { - void showColorPicker(PaletteColor defaultColor, - @NonNull OnColorPickedCallback callback); - - void showDeleteConfirmationScreen( - @NonNull OnConfirmedCallback callback, - int quantity); - - void showEditHabitsScreen(@NonNull List selected); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt new file mode 100644 index 000000000..64c3b8f14 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui.screens.habits.list + +import org.isoron.uhabits.core.commands.ArchiveHabitsCommand +import org.isoron.uhabits.core.commands.ChangeHabitColorCommand +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.DeleteHabitsCommand +import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback +import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback +import javax.inject.Inject + +class ListHabitsSelectionMenuBehavior @Inject constructor( + private val habitList: HabitList, + private val screen: Screen, + private val adapter: Adapter, + var commandRunner: CommandRunner +) { + fun canArchive(): Boolean { + for ((_, _, _, _, isArchived) in adapter.selected) if (isArchived) return false + return true + } + + fun canEdit(): Boolean { + return adapter.selected.size == 1 + } + + fun canUnarchive(): Boolean { + for ((_, _, _, _, isArchived) in adapter.selected) if (!isArchived) return false + return true + } + + fun onArchiveHabits() { + commandRunner.run( + ArchiveHabitsCommand(habitList, adapter.selected) + ) + adapter.clearSelection() + } + + fun onChangeColor() { + val selected = adapter.selected + val (color) = selected[0] + screen.showColorPicker(color) { selectedColor: PaletteColor? -> + commandRunner.run( + ChangeHabitColorCommand(habitList, selected, selectedColor!!) + ) + adapter.clearSelection() + } + } + + fun onDeleteHabits() { + val selected = adapter.selected + screen.showDeleteConfirmationScreen( + { + adapter.performRemove(selected) + commandRunner.run( + DeleteHabitsCommand(habitList, selected) + ) + adapter.clearSelection() + }, + selected.size + ) + } + + fun onEditHabits() { + screen.showEditHabitsScreen(adapter.selected) + adapter.clearSelection() + } + + fun onUnarchiveHabits() { + commandRunner.run( + UnarchiveHabitsCommand(habitList, adapter.selected) + ) + adapter.clearSelection() + } + + interface Adapter { + fun clearSelection() + val selected: List + fun performRemove(selected: List) + } + + interface Screen { + fun showColorPicker( + defaultColor: PaletteColor, + callback: OnColorPickedCallback + ) + + fun showDeleteConfirmationScreen( + callback: OnConfirmedCallback, + quantity: Int + ) + + fun showEditHabitsScreen(selected: List) + } +} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt index 2f1f3c4f6..b81d96284 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt @@ -19,6 +19,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list import com.nhaarman.mockitokotlin2.KArgumentCaptor +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.clearInvocations import com.nhaarman.mockitokotlin2.eq @@ -38,7 +39,6 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers import java.io.IOException import java.nio.file.Files @@ -89,9 +89,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() { @Throws(Exception::class) fun testOnExportCSV() { val outputDir = Files.createTempDirectory("CSV").toFile() - whenever(dirFinder.csvOutputDir).thenReturn(outputDir) + whenever(dirFinder.getCSVOutputDir()).thenReturn(outputDir) behavior.onExportCSV() - verify(screen).showSendFileScreen(ArgumentMatchers.any()) + verify(screen).showSendFileScreen(any()) assertThat(FileUtils.listFiles(outputDir, null, false).size, equalTo(1)) FileUtils.deleteDirectory(outputDir) } @@ -101,7 +101,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() { fun testOnExportCSV_fail() { val outputDir = Files.createTempDirectory("CSV").toFile() outputDir.setWritable(false) - whenever(dirFinder.csvOutputDir).thenReturn(outputDir) + whenever(dirFinder.getCSVOutputDir()).thenReturn(outputDir) behavior.onExportCSV() verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_EXPORT) assertTrue(outputDir.delete()) @@ -131,11 +131,11 @@ class ListHabitsBehaviorTest : BaseUnitTest() { @Test @Throws(IOException::class) fun testOnSendBugReport() { - whenever(bugReporter.bugReport).thenReturn("hello") + whenever(bugReporter.getBugReport()).thenReturn("hello") behavior.onSendBugReport() verify(bugReporter).dumpBugReportToFile() verify(screen).showSendBugReportToDeveloperScreen("hello") - whenever(bugReporter.bugReport).thenThrow(IOException()) + whenever(bugReporter.getBugReport()).thenThrow(IOException()) behavior.onSendBugReport() verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_GENERATE_BUG_REPORT) } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt index 463320d1f..27f54c72f 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt @@ -36,7 +36,6 @@ import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.ThemeSwitcher import org.junit.Test -import org.mockito.ArgumentMatchers class ListHabitsMenuBehaviorTest : BaseUnitTest() { private lateinit var behavior: ListHabitsMenuBehavior @@ -116,7 +115,7 @@ class ListHabitsMenuBehaviorTest : BaseUnitTest() { whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_NAME_ASC) behavior.onSortByStatus() verify(adapter).primaryOrder = orderCaptor.capture() - verify(adapter).setSecondaryOrder(secondaryOrderCaptor.capture()) + verify(adapter).secondaryOrder = secondaryOrderCaptor.capture() assertThat(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_ASC)) assertThat(secondaryOrderCaptor.lastValue, equalTo(HabitList.Order.BY_NAME_ASC)) } @@ -126,7 +125,7 @@ class ListHabitsMenuBehaviorTest : BaseUnitTest() { whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_STATUS_ASC) behavior.onSortByStatus() verify(adapter).primaryOrder = orderCaptor.capture() - verify(adapter, never()).setSecondaryOrder(ArgumentMatchers.any()) + verify(adapter, never()).secondaryOrder assertThat(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_DESC)) } From b21eb3f118b66d795195f9235bd7a967066f5d96 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Tue, 19 Jan 2021 23:34:48 +0100 Subject: [PATCH 07/35] Finish uhabits-core ui conversion --- .../activities/AndroidThemeSwitcher.kt | 8 +- .../habits/list/ListHabitsActivity.kt | 2 +- .../notifications/AndroidNotificationTray.kt | 2 +- .../uhabits/receivers/ReminderController.kt | 2 +- .../uhabits/core/ui/NotificationTray.java | 238 ------------------ .../uhabits/core/ui/NotificationTray.kt | 189 ++++++++++++++ .../isoron/uhabits/core/ui/ThemeSwitcher.java | 95 ------- .../isoron/uhabits/core/ui/ThemeSwitcher.kt | 66 +++++ ...Callback.java => OnColorPickedCallback.kt} | 10 +- ...edCallback.java => OnConfirmedCallback.kt} | 8 +- ...medCallback.java => OnFinishedCallback.kt} | 8 +- .../core/ui/widgets/WidgetBehavior.java | 100 -------- .../uhabits/core/ui/widgets/WidgetBehavior.kt | 79 ++++++ 13 files changed, 351 insertions(+), 456 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.kt rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/{OnColorPickedCallback.java => OnColorPickedCallback.kt} (81%) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/{OnFinishedCallback.java => OnConfirmedCallback.kt} (88%) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/{OnConfirmedCallback.java => OnFinishedCallback.kt} (87%) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt index 0a5e507c4..262db7fda 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt @@ -41,7 +41,7 @@ constructor( preferences: Preferences, ) : ThemeSwitcher(preferences) { - private var currentTheme: Theme = LightTheme() + override var currentTheme: Theme = LightTheme() override fun getSystemTheme(): Int { if (SDK_INT < 29) return THEME_LIGHT @@ -53,9 +53,9 @@ constructor( } } - override fun getCurrentTheme(): Theme { - return currentTheme - } + // override fun getCurrentTheme(): Theme { + // return currentTheme + // } override fun applyDarkTheme() { currentTheme = DarkTheme() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt index ec6cb5e61..a31b78458 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt @@ -33,7 +33,7 @@ import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.sync.SyncManager import org.isoron.uhabits.core.tasks.TaskRunner -import org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_DARK +import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.database.AutoBackup import org.isoron.uhabits.inject.ActivityContextModule 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 711d94547..8f697d490 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 @@ -53,7 +53,7 @@ class AndroidNotificationTray ) : NotificationTray.SystemTray { private var active = HashSet() - override fun log(msg: String) { + override fun log(msg: String?) { Log.d("AndroidNotificationTray", msg) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt index 65956bb2d..7e1e18a46 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt @@ -43,7 +43,7 @@ class ReminderController @Inject constructor( fun onShowReminder( habit: Habit, - timestamp: Timestamp?, + timestamp: Timestamp, reminderTime: Long ) { notificationTray.show(habit, timestamp, reminderTime) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java deleted file mode 100644 index 74c654794..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; - -import javax.inject.*; - - -@AppScope -public class NotificationTray - implements CommandRunner.Listener, Preferences.Listener -{ - public static final String REMINDERS_CHANNEL_ID = "REMINDERS"; - - @NonNull - private final TaskRunner taskRunner; - - @NonNull - private final CommandRunner commandRunner; - - @NonNull - private final Preferences preferences; - - private SystemTray systemTray; - - @NonNull - private final HashMap active; - - @Inject - public NotificationTray(@NonNull TaskRunner taskRunner, - @NonNull CommandRunner commandRunner, - @NonNull Preferences preferences, - @NonNull SystemTray systemTray) - { - this.taskRunner = taskRunner; - this.commandRunner = commandRunner; - this.preferences = preferences; - this.systemTray = systemTray; - this.active = new HashMap<>(); - } - - public void cancel(@NonNull Habit habit) - { - int notificationId = getNotificationId(habit); - systemTray.removeNotification(notificationId); - active.remove(habit); - } - - @Override - public void onCommandFinished(@Nullable Command command) - { - if (command instanceof CreateRepetitionCommand) - { - CreateRepetitionCommand createCmd = (CreateRepetitionCommand) command; - Habit habit = createCmd.getHabit(); - cancel(habit); - } - - if (command instanceof DeleteHabitsCommand) - { - DeleteHabitsCommand deleteCommand = (DeleteHabitsCommand) command; - List deleted = deleteCommand.getSelected(); - for (Habit habit : deleted) - cancel(habit); - } - } - - @Override - public void onNotificationsChanged() - { - reshowAll(); - } - - public void show(@NonNull Habit habit, Timestamp timestamp, long reminderTime) - { - NotificationData data = new NotificationData(timestamp, reminderTime); - active.put(habit, data); - taskRunner.execute(new ShowNotificationTask(habit, data)); - } - - public void startListening() - { - commandRunner.addListener(this); - preferences.addListener(this); - } - - public void stopListening() - { - commandRunner.removeListener(this); - preferences.removeListener(this); - } - - private int getNotificationId(Habit habit) - { - Long id = habit.getId(); - if (id == null) return 0; - return (int) (id % Integer.MAX_VALUE); - } - - private void reshowAll() - { - for (Habit habit : active.keySet()) - { - NotificationData data = active.get(habit); - taskRunner.execute(new ShowNotificationTask(habit, data)); - } - } - - public interface SystemTray - { - void removeNotification(int notificationId); - - void showNotification(Habit habit, - int notificationId, - Timestamp timestamp, - long reminderTime); - - void log(String msg); - } - - static class NotificationData - { - public final Timestamp timestamp; - - public final long reminderTime; - - public NotificationData(Timestamp timestamp, long reminderTime) - { - this.timestamp = timestamp; - this.reminderTime = reminderTime; - } - } - - private class ShowNotificationTask implements Task - { - int todayValue; - - private final Habit habit; - - private final Timestamp timestamp; - - private final long reminderTime; - - public ShowNotificationTask(Habit habit, NotificationData data) - { - this.habit = habit; - this.timestamp = data.timestamp; - this.reminderTime = data.reminderTime; - } - - @Override - public void doInBackground() - { - Timestamp today = DateUtils.getTodayWithOffset(); - todayValue = habit.getComputedEntries().get(today).getValue(); - } - - @Override - public void onPostExecute() - { - systemTray.log("Showing notification for habit=" + habit.getId()); - - if (todayValue != Entry.UNKNOWN) { - systemTray.log(String.format( - Locale.US, - "Habit %d already checked. Skipping.", - habit.getId())); - return; - } - - if (!habit.hasReminder()) { - systemTray.log(String.format( - Locale.US, - "Habit %d does not have a reminder. Skipping.", - habit.getId())); - return; - } - - if (habit.isArchived()) - { - systemTray.log(String.format( - Locale.US, - "Habit %d is archived. Skipping.", - habit.getId())); - return; - } - - if (!shouldShowReminderToday()) { - systemTray.log(String.format( - Locale.US, - "Habit %d not supposed to run today. Skipping.", - habit.getId())); - return; - } - - systemTray.showNotification(habit, getNotificationId(habit), timestamp, - reminderTime); - } - - private boolean shouldShowReminderToday() - { - if (!habit.hasReminder()) return false; - Reminder reminder = habit.getReminder(); - - boolean[] reminderDays = Objects.requireNonNull(reminder).getDays().toArray(); - int weekday = timestamp.getWeekday(); - - return reminderDays[weekday]; - } - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt new file mode 100644 index 000000000..666947f85 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui + +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.core.commands.Command +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.commands.DeleteHabitsCommand +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.tasks.Task +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.HashMap +import java.util.Locale +import java.util.Objects +import javax.inject.Inject + +@AppScope +class NotificationTray @Inject constructor( + private val taskRunner: TaskRunner, + private val commandRunner: CommandRunner, + private val preferences: Preferences, + private val systemTray: SystemTray +) : CommandRunner.Listener, Preferences.Listener { + private val active: HashMap + fun cancel(habit: Habit) { + val notificationId = getNotificationId(habit) + systemTray.removeNotification(notificationId) + active.remove(habit) + } + + override fun onCommandFinished(command: Command) { + if (command is CreateRepetitionCommand) { + val (_, habit) = command + cancel(habit) + } + if (command is DeleteHabitsCommand) { + val (_, deleted) = command + for (habit in deleted) cancel(habit) + } + } + + override fun onNotificationsChanged() { + reshowAll() + } + + fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { + val data = NotificationData(timestamp, reminderTime) + active[habit] = data + taskRunner.execute(ShowNotificationTask(habit, data)) + } + + fun startListening() { + commandRunner.addListener(this) + preferences.addListener(this) + } + + fun stopListening() { + commandRunner.removeListener(this) + preferences.removeListener(this) + } + + private fun getNotificationId(habit: Habit): Int { + val id = habit.id ?: return 0 + return (id % Int.MAX_VALUE).toInt() + } + + private fun reshowAll() { + for (habit in active.keys) { + val data = active[habit]!! + taskRunner.execute(ShowNotificationTask(habit, data)) + } + } + + interface SystemTray { + fun removeNotification(notificationId: Int) + fun showNotification( + habit: Habit, + notificationId: Int, + timestamp: Timestamp, + reminderTime: Long + ) + + fun log(msg: String?) + } + + internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) + private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : + Task { + var todayValue = 0 + private val timestamp: Timestamp + private val reminderTime: Long + override fun doInBackground() { + val today = getTodayWithOffset() + todayValue = habit.computedEntries.get(today).value + } + + override fun onPostExecute() { + systemTray.log("Showing notification for habit=" + habit.id) + if (todayValue != Entry.UNKNOWN) { + systemTray.log( + String.format( + Locale.US, + "Habit %d already checked. Skipping.", + habit.id + ) + ) + return + } + if (!habit.hasReminder()) { + systemTray.log( + String.format( + Locale.US, + "Habit %d does not have a reminder. Skipping.", + habit.id + ) + ) + return + } + if (habit.isArchived) { + systemTray.log( + String.format( + Locale.US, + "Habit %d is archived. Skipping.", + habit.id + ) + ) + return + } + if (!shouldShowReminderToday()) { + systemTray.log( + String.format( + Locale.US, + "Habit %d not supposed to run today. Skipping.", + habit.id + ) + ) + return + } + systemTray.showNotification( + habit, + getNotificationId(habit), + timestamp, + reminderTime + ) + } + + private fun shouldShowReminderToday(): Boolean { + if (!habit.hasReminder()) return false + val reminder = habit.reminder + val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray() + val weekday = timestamp.weekday + return reminderDays[weekday] + } + + init { + timestamp = data.timestamp + reminderTime = data.reminderTime + } + } + + companion object { + const val REMINDERS_CHANNEL_ID = "REMINDERS" + } + + init { + active = HashMap() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.java deleted file mode 100644 index ac9fd8dff..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.ui.views.*; - -public abstract class ThemeSwitcher -{ - public static final int THEME_DARK = 1; - - public static final int THEME_LIGHT = 2; - - public static final int THEME_AUTOMATIC = 0; - - private final Preferences preferences; - - public ThemeSwitcher(@NonNull Preferences preferences) - { - this.preferences = preferences; - } - - public void apply() - { - if (isNightMode()) - { - if (preferences.isPureBlackEnabled()) applyPureBlackTheme(); - else applyDarkTheme(); - } - else - { - applyLightTheme(); - } - } - - public abstract void applyDarkTheme(); - - public abstract void applyLightTheme(); - - public abstract void applyPureBlackTheme(); - - public abstract int getSystemTheme(); - - public abstract Theme getCurrentTheme(); - - public boolean isNightMode() - { - int systemTheme = getSystemTheme(); - int userTheme = preferences.getTheme(); - - return (userTheme == THEME_DARK || - (systemTheme == THEME_DARK && userTheme == THEME_AUTOMATIC)); - } - - public void toggleNightMode() - { - int systemTheme = getSystemTheme(); - int userTheme = preferences.getTheme(); - - if(userTheme == THEME_AUTOMATIC) - { - if(systemTheme == THEME_LIGHT) preferences.setTheme(THEME_DARK); - if(systemTheme == THEME_DARK) preferences.setTheme(THEME_LIGHT); - } - else if(userTheme == THEME_LIGHT) - { - if (systemTheme == THEME_LIGHT) preferences.setTheme(THEME_DARK); - if (systemTheme == THEME_DARK) preferences.setTheme(THEME_AUTOMATIC); - } - else if(userTheme == THEME_DARK) - { - if (systemTheme == THEME_LIGHT) preferences.setTheme(THEME_AUTOMATIC); - if (systemTheme == THEME_DARK) preferences.setTheme(THEME_LIGHT); - } - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.kt new file mode 100644 index 000000000..8ad3eb421 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/ThemeSwitcher.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui + +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.views.Theme + +abstract class ThemeSwitcher(private val preferences: Preferences) { + fun apply() { + if (isNightMode) { + if (preferences.isPureBlackEnabled) applyPureBlackTheme() else applyDarkTheme() + } else { + applyLightTheme() + } + } + + abstract fun applyDarkTheme() + abstract fun applyLightTheme() + abstract fun applyPureBlackTheme() + abstract fun getSystemTheme(): Int + abstract val currentTheme: Theme? + val isNightMode: Boolean + get() { + val systemTheme = getSystemTheme() + val userTheme = preferences.theme + return userTheme == THEME_DARK || + systemTheme == THEME_DARK && userTheme == THEME_AUTOMATIC + } + + fun toggleNightMode() { + val systemTheme = getSystemTheme() + val userTheme = preferences.theme + if (userTheme == THEME_AUTOMATIC) { + if (systemTheme == THEME_LIGHT) preferences.theme = THEME_DARK + if (systemTheme == THEME_DARK) preferences.theme = THEME_LIGHT + } else if (userTheme == THEME_LIGHT) { + if (systemTheme == THEME_LIGHT) preferences.theme = THEME_DARK + if (systemTheme == THEME_DARK) preferences.theme = THEME_AUTOMATIC + } else if (userTheme == THEME_DARK) { + if (systemTheme == THEME_LIGHT) preferences.theme = THEME_AUTOMATIC + if (systemTheme == THEME_DARK) preferences.theme = THEME_LIGHT + } + } + + companion object { + const val THEME_DARK = 1 + const val THEME_LIGHT = 2 + const val THEME_AUTOMATIC = 0 + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.kt similarity index 81% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.kt index a34b48ac7..1129fb973 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnColorPickedCallback.kt @@ -16,12 +16,10 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.ui.callbacks -package org.isoron.uhabits.core.ui.callbacks; +import org.isoron.uhabits.core.models.PaletteColor -import org.isoron.uhabits.core.models.*; - -public interface OnColorPickedCallback -{ - void onColorPicked(PaletteColor color); +fun interface OnColorPickedCallback { + fun onColorPicked(color: PaletteColor) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.kt similarity index 88% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.kt index 190232e31..5d6ad813a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.kt @@ -16,10 +16,8 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.ui.callbacks -package org.isoron.uhabits.core.ui.callbacks; - -public interface OnFinishedCallback -{ - void onFinish(); +fun interface OnConfirmedCallback { + fun onConfirmed() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.kt similarity index 87% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.kt index b43277728..1da8ce98d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnConfirmedCallback.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnFinishedCallback.kt @@ -16,10 +16,8 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.ui.callbacks -package org.isoron.uhabits.core.ui.callbacks; - -public interface OnConfirmedCallback -{ - void onConfirmed(); +interface OnFinishedCallback { + fun onFinish() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java deleted file mode 100644 index 1499674c0..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.ui.widgets; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.ui.*; -import org.jetbrains.annotations.*; - -import javax.inject.*; - -import static org.isoron.uhabits.core.models.Entry.*; - -public class WidgetBehavior -{ - private HabitList habitList; - - @NonNull - private final CommandRunner commandRunner; - - @NonNull - private final NotificationTray notificationTray; - - @NonNull - private final Preferences preferences; - - @Inject - public WidgetBehavior(@NonNull HabitList habitList, - @NonNull CommandRunner commandRunner, - @NonNull NotificationTray notificationTray, - @NonNull Preferences preferences) - { - this.habitList = habitList; - this.commandRunner = commandRunner; - this.notificationTray = notificationTray; - this.preferences = preferences; - } - - public void onAddRepetition(@NonNull Habit habit, Timestamp timestamp) - { - notificationTray.cancel(habit); - setValue(habit, timestamp, YES_MANUAL); - } - - public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp) - { - notificationTray.cancel(habit); - setValue(habit, timestamp, NO); - } - - public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp) - { - int currentValue = habit.getOriginalEntries().get(timestamp).getValue(); - int newValue; - if(preferences.isSkipEnabled()) - newValue = Entry.Companion.nextToggleValueWithSkip(currentValue); - else - newValue = Entry.Companion.nextToggleValueWithoutSkip(currentValue); - setValue(habit, timestamp, newValue); - notificationTray.cancel(habit); - } - - public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { - int currentValue = habit.getComputedEntries().get(timestamp).getValue(); - setValue(habit, timestamp, currentValue + amount); - notificationTray.cancel(habit); - } - - public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { - int currentValue = habit.getComputedEntries().get(timestamp).getValue(); - setValue(habit, timestamp, currentValue - amount); - notificationTray.cancel(habit); - } - - public void setValue(@NonNull Habit habit, Timestamp timestamp, int newValue) { - commandRunner.run( - new CreateRepetitionCommand(habitList, habit, timestamp, newValue) - ); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt new file mode 100644 index 000000000..99bad0e84 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.ui.widgets + +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.models.Entry.Companion.nextToggleValueWithSkip +import org.isoron.uhabits.core.models.Entry.Companion.nextToggleValueWithoutSkip +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.NotificationTray +import javax.inject.Inject + +class WidgetBehavior @Inject constructor( + private val habitList: HabitList, + private val commandRunner: CommandRunner, + private val notificationTray: NotificationTray, + private val preferences: Preferences +) { + fun onAddRepetition(habit: Habit, timestamp: Timestamp?) { + notificationTray.cancel(habit) + setValue(habit, timestamp, Entry.YES_MANUAL) + } + + fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) { + notificationTray.cancel(habit) + setValue(habit, timestamp, Entry.NO) + } + + fun onToggleRepetition(habit: Habit, timestamp: Timestamp?) { + val currentValue = habit.originalEntries.get(timestamp!!).value + val newValue: Int + newValue = + if (preferences.isSkipEnabled) nextToggleValueWithSkip( + currentValue + ) else nextToggleValueWithoutSkip( + currentValue + ) + setValue(habit, timestamp, newValue) + notificationTray.cancel(habit) + } + + fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) { + val currentValue = habit.computedEntries.get(timestamp).value + setValue(habit, timestamp, currentValue + amount) + notificationTray.cancel(habit) + } + + fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) { + val currentValue = habit.computedEntries.get(timestamp).value + setValue(habit, timestamp, currentValue - amount) + notificationTray.cancel(habit) + } + + fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int) { + commandRunner.run( + CreateRepetitionCommand(habitList, habit, timestamp!!, newValue) + ) + } +} From 8f5f72d9fd3cb889d768c8981861b59819b40118 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 14:04:43 +0100 Subject: [PATCH 08/35] Convert ReminderScheduler --- .../isoron/uhabits/intents/IntentScheduler.kt | 2 +- .../SnoozeDelayPickerActivity.kt | 2 +- .../uhabits/receivers/ReminderController.kt | 4 +- .../core/reminders/ReminderScheduler.java | 176 ------------------ .../core/reminders/ReminderScheduler.kt | 150 +++++++++++++++ 5 files changed, 154 insertions(+), 180 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt index 5a4c79f1d..5d699944c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt @@ -80,7 +80,7 @@ class IntentScheduler return schedule(updateTime, intent, RTC) } - override fun log(componentName: String, msg: String) { + override fun log(componentName: String?, msg: String?) { Log.d(componentName, msg) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt index bc3afb97c..eeb0730b9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt @@ -79,7 +79,7 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener { override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) { val snoozeValues = resources.getIntArray(R.array.snooze_picker_values) if (snoozeValues[position] >= 0) { - reminderController!!.onSnoozeDelayPicked(habit, snoozeValues[position]) + reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position]) finish() } else showTimePicker() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt index 7e1e18a46..38ba3a1b4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt @@ -54,9 +54,9 @@ class ReminderController @Inject constructor( showSnoozeDelayPicker(habit, context) } - fun onSnoozeDelayPicked(habit: Habit?, delayInMinutes: Int) { + fun onSnoozeDelayPicked(habit: Habit, delayInMinutes: Int) { reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong()) - notificationTray.cancel(habit!!) + notificationTray.cancel(habit) } fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java deleted file mode 100644 index 5f93c32c7..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.reminders; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; - -import java.util.*; - -import javax.inject.*; - -import static org.isoron.uhabits.core.utils.DateUtils.*; - -@AppScope -public class ReminderScheduler implements CommandRunner.Listener -{ - private final WidgetPreferences widgetPreferences; - - private CommandRunner commandRunner; - - private HabitList habitList; - - private SystemScheduler sys; - - @Inject - public ReminderScheduler(@NonNull CommandRunner commandRunner, - @NonNull HabitList habitList, - @NonNull SystemScheduler sys, - @NonNull WidgetPreferences widgetPreferences) - { - this.commandRunner = commandRunner; - this.habitList = habitList; - this.sys = sys; - this.widgetPreferences = widgetPreferences; - } - - @Override - public synchronized void onCommandFinished(@Nullable Command command) - { - if (command instanceof CreateRepetitionCommand) return; - if (command instanceof ChangeHabitColorCommand) return; - scheduleAll(); - } - - public synchronized void schedule(@NonNull Habit habit) - { - if (habit.getId() == null) - { - sys.log("ReminderScheduler", "Habit has null id. Returning."); - return; - } - - if (!habit.hasReminder()) - { - sys.log("ReminderScheduler", "habit=" + habit.getId() + " has no reminder. Skipping."); - return; - } - - long reminderTime = Objects.requireNonNull(habit.getReminder()).getTimeInMillis(); - long snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.getId()); - - if (snoozeReminderTime != 0) - { - long now = applyTimezone(getLocalTime()); - sys.log("ReminderScheduler", String.format( - Locale.US, - "Habit %d has been snoozed until %d", - habit.getId(), - snoozeReminderTime)); - - if (snoozeReminderTime > now) - { - sys.log("ReminderScheduler", "Snooze time is in the future. Accepting."); - reminderTime = snoozeReminderTime; - } - else - { - sys.log("ReminderScheduler", "Snooze time is in the past. Discarding."); - widgetPreferences.removeSnoozeTime(habit.getId()); - } - } - scheduleAtTime(habit, reminderTime); - - } - - public synchronized void scheduleAtTime(@NonNull Habit habit, long reminderTime) - { - sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.getId()); - - if (!habit.hasReminder()) - { - sys.log("ReminderScheduler", "habit=" + habit.getId() + " has no reminder. Skipping."); - return; - } - - if (habit.isArchived()) - { - sys.log("ReminderScheduler", "habit=" + habit.getId() + " is archived. Skipping."); - return; - } - - long timestamp = getStartOfDayWithOffset(removeTimezone(reminderTime)); - sys.log("ReminderScheduler", - String.format( - Locale.US, - "reminderTime=%d removeTimezone=%d timestamp=%d", - reminderTime, - removeTimezone(reminderTime), - timestamp)); - - sys.scheduleShowReminder(reminderTime, habit, timestamp); - } - - public synchronized void scheduleAll() - { - sys.log("ReminderScheduler", "Scheduling all alarms"); - HabitList reminderHabits = - habitList.getFiltered(HabitMatcher.WITH_ALARM); - for (Habit habit : reminderHabits) - schedule(habit); - } - - public synchronized void startListening() - { - commandRunner.addListener(this); - } - - public synchronized void stopListening() - { - commandRunner.removeListener(this); - } - - public synchronized void snoozeReminder(Habit habit, long minutes) - { - long now = applyTimezone(getLocalTime()); - long snoozedUntil = now + minutes * 60 * 1000; - widgetPreferences.setSnoozeTime(habit.getId(), snoozedUntil); - schedule(habit); - } - - public interface SystemScheduler - { - SchedulerResult scheduleShowReminder(long reminderTime, Habit habit, long timestamp); - - SchedulerResult scheduleWidgetUpdate(long updateTime); - - void log(String componentName, String msg); - } - - public enum SchedulerResult - { - IGNORED, - OK - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt new file mode 100644 index 000000000..7f602e11a --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.reminders + +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.core.commands.ChangeHabitColorCommand +import org.isoron.uhabits.core.commands.Command +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.preferences.WidgetPreferences +import org.isoron.uhabits.core.utils.DateUtils.Companion.applyTimezone +import org.isoron.uhabits.core.utils.DateUtils.Companion.getLocalTime +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfDayWithOffset +import org.isoron.uhabits.core.utils.DateUtils.Companion.removeTimezone +import java.util.Locale +import java.util.Objects +import javax.inject.Inject + +@AppScope +class ReminderScheduler @Inject constructor( + private val commandRunner: CommandRunner, + private val habitList: HabitList, + private val sys: SystemScheduler, + private val widgetPreferences: WidgetPreferences +) : CommandRunner.Listener { + @Synchronized + override fun onCommandFinished(command: Command) { + if (command is CreateRepetitionCommand) return + if (command is ChangeHabitColorCommand) return + scheduleAll() + } + + @Synchronized + fun schedule(habit: Habit) { + if (habit.id == null) { + sys.log("ReminderScheduler", "Habit has null id. Returning.") + return + } + if (!habit.hasReminder()) { + sys.log("ReminderScheduler", "habit=" + habit.id + " has no reminder. Skipping.") + return + } + var reminderTime = Objects.requireNonNull(habit.reminder)!!.timeInMillis + val snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.id!!) + if (snoozeReminderTime != 0L) { + val now = applyTimezone(getLocalTime()) + sys.log( + "ReminderScheduler", + String.format( + Locale.US, + "Habit %d has been snoozed until %d", + habit.id, + snoozeReminderTime + ) + ) + if (snoozeReminderTime > now) { + sys.log("ReminderScheduler", "Snooze time is in the future. Accepting.") + reminderTime = snoozeReminderTime + } else { + sys.log("ReminderScheduler", "Snooze time is in the past. Discarding.") + widgetPreferences.removeSnoozeTime(habit.id!!) + } + } + scheduleAtTime(habit, reminderTime) + } + + @Synchronized + fun scheduleAtTime(habit: Habit, reminderTime: Long) { + sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.id) + if (!habit.hasReminder()) { + sys.log("ReminderScheduler", "habit=" + habit.id + " has no reminder. Skipping.") + return + } + if (habit.isArchived) { + sys.log("ReminderScheduler", "habit=" + habit.id + " is archived. Skipping.") + return + } + val timestamp = getStartOfDayWithOffset(removeTimezone(reminderTime)) + sys.log( + "ReminderScheduler", + String.format( + Locale.US, + "reminderTime=%d removeTimezone=%d timestamp=%d", + reminderTime, + removeTimezone(reminderTime), + timestamp + ) + ) + sys.scheduleShowReminder(reminderTime, habit, timestamp) + } + + @Synchronized + fun scheduleAll() { + sys.log("ReminderScheduler", "Scheduling all alarms") + val reminderHabits = habitList.getFiltered(HabitMatcher.WITH_ALARM) + for (habit in reminderHabits) schedule(habit) + } + + @Synchronized + fun startListening() { + commandRunner.addListener(this) + } + + @Synchronized + fun stopListening() { + commandRunner.removeListener(this) + } + + @Synchronized + fun snoozeReminder(habit: Habit, minutes: Long) { + val now = applyTimezone(getLocalTime()) + val snoozedUntil = now + minutes * 60 * 1000 + widgetPreferences.setSnoozeTime(habit.id, snoozedUntil) + schedule(habit) + } + + interface SystemScheduler { + fun scheduleShowReminder( + reminderTime: Long, + habit: Habit, + timestamp: Long + ): SchedulerResult + + fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult? + fun log(componentName: String?, msg: String?) + } + + enum class SchedulerResult { + IGNORED, OK + } +} From 39cec6f11da4585d99daa75e975c884730ba98d0 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Mon, 25 Jan 2021 10:56:37 +0100 Subject: [PATCH 09/35] Convert Preferences --- .../uhabits/widgets/BaseWidgetProvider.kt | 2 +- .../isoron/uhabits/widgets/WidgetUpdater.kt | 2 +- .../uhabits/core/preferences/Preferences.java | 439 ------------------ .../uhabits/core/preferences/Preferences.kt | 294 ++++++++++++ .../core/preferences/PropertiesStorage.java | 134 ------ .../core/preferences/PropertiesStorage.kt | 99 ++++ .../core/preferences/WidgetPreferences.java | 82 ---- .../core/preferences/WidgetPreferences.kt | 69 +++ .../core/reminders/ReminderScheduler.kt | 2 +- .../core/ui/screens/habits/list/HintList.kt | 2 +- 10 files changed, 466 insertions(+), 659 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt index 1a41d0f4f..6fae9fc9c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.kt @@ -109,7 +109,7 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { } protected fun getHabitsFromWidgetId(widgetId: Int): List { - val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId) + val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)!! val selectedHabits = ArrayList(selectedIds.size) for (id in selectedIds) { val h = habits.getById(id) ?: throw HabitNotFoundException() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt index d577727e0..bd6d7a50e 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt @@ -95,7 +95,7 @@ class WidgetUpdater val modifiedWidgetIds = when (modifiedHabitId) { null -> widgetIds.toList() else -> widgetIds.filter { w -> - widgetPrefs.getHabitIdsFromWidgetId(w).contains(modifiedHabitId) + widgetPrefs.getHabitIdsFromWidgetId(w)!!.contains(modifiedHabitId) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.java deleted file mode 100644 index 262235526..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.preferences; - -import androidx.annotation.*; - -import org.isoron.platform.time.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.ui.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; - -public class Preferences -{ - @NonNull - private final Storage storage; - - @NonNull - private List listeners; - - @Nullable - private Boolean shouldReverseCheckmarks = null; - public Preferences(@NonNull Storage storage) - { - this.storage = storage; - listeners = new LinkedList<>(); - storage.onAttached(this); - } - - public void addListener(Listener listener) - { - listeners.add(listener); - } - - public Integer getDefaultHabitColor(int fallbackColor) - { - return storage.getInt("pref_default_habit_palette_color", - fallbackColor); - } - - public HabitList.Order getDefaultPrimaryOrder() - { - String name = storage.getString("pref_default_order", "BY_POSITION"); - - try - { - return HabitList.Order.valueOf(name); - } - catch (IllegalArgumentException e) - { - setDefaultPrimaryOrder(HabitList.Order.BY_POSITION); - return HabitList.Order.BY_POSITION; - } - } - - public HabitList.Order getDefaultSecondaryOrder() { - String name = storage.getString("pref_default_secondary_order", "BY_NAME_ASC"); - - try - { - return HabitList.Order.valueOf(name); - } - catch (IllegalArgumentException e) - { - setDefaultSecondaryOrder(HabitList.Order.BY_NAME_ASC); - return HabitList.Order.BY_POSITION; - } - } - - public void setDefaultPrimaryOrder(HabitList.Order order) - { - storage.putString("pref_default_order", order.name()); - } - - public void setDefaultSecondaryOrder(HabitList.Order order) - { - storage.putString("pref_default_secondary_order", order.name()); - } - - public int getScoreCardSpinnerPosition() - { - return Math.min(4, Math.max(0, storage.getInt("pref_score_view_interval", 1))); - } - - public void setScoreCardSpinnerPosition(int position) - { - storage.putInt("pref_score_view_interval", position); - } - - public int getBarCardBoolSpinnerPosition() - { - return Math.min(3, Math.max(0, storage.getInt("pref_bar_card_bool_spinner", 0))); - } - - public void setBarCardBoolSpinnerPosition(int position) - { - storage.putInt("pref_bar_card_bool_spinner", position); - } - - public int getBarCardNumericalSpinnerPosition() - { - return Math.min(4, Math.max(0, storage.getInt("pref_bar_card_numerical_spinner", 0))); - } - - public void setBarCardNumericalSpinnerPosition(int position) - { - storage.putInt("pref_bar_card_numerical_spinner", position); - } - - public int getLastHintNumber() - { - return storage.getInt("last_hint_number", -1); - } - - public Timestamp getLastHintTimestamp() - { - long unixTime = storage.getLong("last_hint_timestamp", -1); - if (unixTime < 0) return null; - else return new Timestamp(unixTime); - } - - public boolean getShowArchived() - { - return storage.getBoolean("pref_show_archived", false); - } - - public void setShowArchived(boolean showArchived) - { - storage.putBoolean("pref_show_archived", showArchived); - } - - public boolean getShowCompleted() - { - return storage.getBoolean("pref_show_completed", true); - } - - public void setShowCompleted(boolean showCompleted) - { - storage.putBoolean("pref_show_completed", showCompleted); - } - - public long getSnoozeInterval() - { - return Long.parseLong(storage.getString("pref_snooze_interval", "15")); - } - - public void setSnoozeInterval(int interval) - { - storage.putString("pref_snooze_interval", String.valueOf(interval)); - } - - public int getTheme() - { - return storage.getInt("pref_theme", ThemeSwitcher.THEME_AUTOMATIC); - } - - public void setTheme(int theme) - { - storage.putInt("pref_theme", theme); - } - - public void incrementLaunchCount() - { - storage.putInt("launch_count", getLaunchCount() + 1); - } - - public int getLaunchCount() - { - return storage.getInt("launch_count", 0); - } - - public boolean isDeveloper() - { - return storage.getBoolean("pref_developer", false); - } - - public void setDeveloper(boolean isDeveloper) - { - storage.putBoolean("pref_developer", isDeveloper); - } - - public boolean isFirstRun() - { - return storage.getBoolean("pref_first_run", true); - } - - public void setFirstRun(boolean isFirstRun) - { - storage.putBoolean("pref_first_run", isFirstRun); - } - - public boolean isPureBlackEnabled() - { - return storage.getBoolean("pref_pure_black", false); - } - - public void setPureBlackEnabled(boolean enabled) - { - storage.putBoolean("pref_pure_black", enabled); - } - - public boolean isShortToggleEnabled() - { - return storage.getBoolean("pref_short_toggle", false); - } - - public void setShortToggleEnabled(boolean enabled) - { - storage.putBoolean("pref_short_toggle", enabled); - } - - public void removeListener(Listener listener) - { - listeners.remove(listener); - } - - public void clear() - { - storage.clear(); - } - - public void setDefaultHabitColor(int color) - { - storage.putInt("pref_default_habit_palette_color", color); - } - - public void setNotificationsSticky(boolean sticky) - { - storage.putBoolean("pref_sticky_notifications", sticky); - for (Listener l : listeners) l.onNotificationsChanged(); - } - - public void setNotificationsLed(boolean enabled) - { - storage.putBoolean("pref_led_notifications", enabled); - for (Listener l : listeners) l.onNotificationsChanged(); - } - - public boolean shouldMakeNotificationsSticky() - { - return storage.getBoolean("pref_sticky_notifications", false); - } - - public boolean shouldMakeNotificationsLed() - { - return storage.getBoolean("pref_led_notifications", false); - } - - public boolean isCheckmarkSequenceReversed() - { - if (shouldReverseCheckmarks == null) shouldReverseCheckmarks = - storage.getBoolean("pref_checkmark_reverse_order", false); - - return shouldReverseCheckmarks; - } - - public void setCheckmarkSequenceReversed(boolean reverse) - { - shouldReverseCheckmarks = reverse; - storage.putBoolean("pref_checkmark_reverse_order", reverse); - for (Listener l : listeners) l.onCheckmarkSequenceChanged(); - } - - public void updateLastHint(int number, Timestamp timestamp) - { - storage.putInt("last_hint_number", number); - storage.putLong("last_hint_timestamp", timestamp.getUnixTime()); - } - - public int getLastAppVersion() - { - return storage.getInt("last_version", 0); - } - - public void setLastAppVersion(int version) - { - storage.putInt("last_version", version); - } - - public int getWidgetOpacity() - { - return Integer.parseInt(storage.getString("pref_widget_opacity", "255")); - } - - public void setWidgetOpacity(int value) - { - storage.putString("pref_widget_opacity", Integer.toString(value)); - } - - public boolean isSkipEnabled() - { - return storage.getBoolean("pref_skip_enabled", false); - } - - public void setSkipEnabled(boolean value) - { - storage.putBoolean("pref_skip_enabled", value); - } - - public String getSyncBaseURL() - { - return storage.getString("pref_sync_base_url", ""); - } - - public String getSyncKey() - { - return storage.getString("pref_sync_key", ""); - } - - public String getEncryptionKey() - { - return storage.getString("pref_encryption_key", ""); - } - - public boolean isSyncEnabled() - { - return storage.getBoolean("pref_sync_enabled", false); - } - - public void enableSync(String syncKey, String encKey) - { - storage.putBoolean("pref_sync_enabled", true); - storage.putString("pref_sync_key", syncKey); - storage.putString("pref_encryption_key", encKey); - for (Listener l : listeners) l.onSyncEnabled(); - } - - public void disableSync() - { - storage.putBoolean("pref_sync_enabled", false); - storage.putString("pref_sync_key", ""); - storage.putString("pref_encryption_key", ""); - } - - public boolean areQuestionMarksEnabled() - { - return storage.getBoolean("pref_unknown_enabled", false); - } - - /** - * @return An integer representing the first day of the week. Sunday - * corresponds to 1, Monday to 2, and so on, until Saturday, which is - * represented by 7. By default, this is based on the current system locale, - * unless the user changed this in the settings. - */ - @Deprecated() - public int getFirstWeekdayInt() - { - String weekday = storage.getString("pref_first_weekday", ""); - if (weekday.isEmpty()) return DateUtils.getFirstWeekdayNumberAccordingToLocale(); - return Integer.parseInt(weekday); - } - - public DayOfWeek getFirstWeekday() - { - int weekday = Integer.parseInt(storage.getString("pref_first_weekday", "-1")); - if (weekday < 0) weekday = DateUtils.getFirstWeekdayNumberAccordingToLocale(); - switch (weekday) { - case 1: return DayOfWeek.SUNDAY; - case 2: return DayOfWeek.MONDAY; - case 3: return DayOfWeek.TUESDAY; - case 4: return DayOfWeek.WEDNESDAY; - case 5: return DayOfWeek.THURSDAY; - case 6: return DayOfWeek.FRIDAY; - case 7: return DayOfWeek.SATURDAY; - default: throw new IllegalArgumentException(); - } - } - - public interface Listener - { - default void onCheckmarkSequenceChanged() - { - } - - default void onNotificationsChanged() - { - } - - default void onSyncEnabled() - { - } - } - - public interface Storage - { - void clear(); - - boolean getBoolean(String key, boolean defValue); - - int getInt(String key, int defValue); - - long getLong(String key, long defValue); - - String getString(String key, String defValue); - - void onAttached(Preferences preferences); - - void putBoolean(String key, boolean value); - - void putInt(String key, int value); - - void putLong(String key, long value); - - void putString(String key, String value); - - void remove(String key); - - default void putLongArray(String key, long[] values) - { - putString(key, StringUtils.joinLongs(values)); - } - - default long[] getLongArray(String key, long[] defValue) - { - String string = getString(key, ""); - if (string.isEmpty()) return defValue; - else return StringUtils.splitLongs(string); - } - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt new file mode 100644 index 000000000..43fd47a24 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.preferences + +import org.isoron.platform.time.DayOfWeek +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.ui.ThemeSwitcher +import org.isoron.uhabits.core.utils.DateUtils.Companion.getFirstWeekdayNumberAccordingToLocale +import org.isoron.uhabits.core.utils.StringUtils.Companion.joinLongs +import org.isoron.uhabits.core.utils.StringUtils.Companion.splitLongs +import java.util.LinkedList +import kotlin.math.max +import kotlin.math.min + +class Preferences(private val storage: Storage) { + private val listeners: MutableList + private var shouldReverseCheckmarks: Boolean? = null + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun getDefaultHabitColor(fallbackColor: Int): Int { + return storage.getInt( + "pref_default_habit_palette_color", + fallbackColor + ) + } + + var defaultPrimaryOrder: HabitList.Order + get() { + val name = storage.getString("pref_default_order", "BY_POSITION") + return try { + HabitList.Order.valueOf(name) + } catch (e: IllegalArgumentException) { + defaultPrimaryOrder = HabitList.Order.BY_POSITION + HabitList.Order.BY_POSITION + } + } + set(order) { + storage.putString("pref_default_order", order.name) + } + var defaultSecondaryOrder: HabitList.Order + get() { + val name = storage.getString("pref_default_secondary_order", "BY_NAME_ASC") + return try { + HabitList.Order.valueOf(name) + } catch (e: IllegalArgumentException) { + defaultSecondaryOrder = HabitList.Order.BY_NAME_ASC + HabitList.Order.BY_POSITION + } + } + set(order) { + storage.putString("pref_default_secondary_order", order.name) + } + var scoreCardSpinnerPosition: Int + get() = min(4, max(0, storage.getInt("pref_score_view_interval", 1))) + set(position) { + storage.putInt("pref_score_view_interval", position) + } + var barCardBoolSpinnerPosition: Int + get() = min(3, max(0, storage.getInt("pref_bar_card_bool_spinner", 0))) + set(position) { + storage.putInt("pref_bar_card_bool_spinner", position) + } + var barCardNumericalSpinnerPosition: Int + get() = min(4, max(0, storage.getInt("pref_bar_card_numerical_spinner", 0))) + set(position) { + storage.putInt("pref_bar_card_numerical_spinner", position) + } + val lastHintNumber: Int + get() = storage.getInt("last_hint_number", -1) + val lastHintTimestamp: Timestamp? + get() { + val unixTime = storage.getLong("last_hint_timestamp", -1) + return if (unixTime < 0) null else Timestamp(unixTime) + } + var showArchived: Boolean + get() = storage.getBoolean("pref_show_archived", false) + set(showArchived) { + storage.putBoolean("pref_show_archived", showArchived) + } + var showCompleted: Boolean + get() = storage.getBoolean("pref_show_completed", true) + set(showCompleted) { + storage.putBoolean("pref_show_completed", showCompleted) + } + val snoozeInterval: Long + get() = storage.getString("pref_snooze_interval", "15").toLong() + + fun setSnoozeInterval(interval: Int) { + storage.putString("pref_snooze_interval", interval.toString()) + } + + var theme: Int + get() = storage.getInt("pref_theme", ThemeSwitcher.THEME_AUTOMATIC) + set(theme) { + storage.putInt("pref_theme", theme) + } + + fun incrementLaunchCount() { + storage.putInt("launch_count", launchCount + 1) + } + + val launchCount: Int + get() = storage.getInt("launch_count", 0) + var isDeveloper: Boolean + get() = storage.getBoolean("pref_developer", false) + set(isDeveloper) { + storage.putBoolean("pref_developer", isDeveloper) + } + var isFirstRun: Boolean + get() = storage.getBoolean("pref_first_run", true) + set(isFirstRun) { + storage.putBoolean("pref_first_run", isFirstRun) + } + var isPureBlackEnabled: Boolean + get() = storage.getBoolean("pref_pure_black", false) + set(enabled) { + storage.putBoolean("pref_pure_black", enabled) + } + var isShortToggleEnabled: Boolean + get() = storage.getBoolean("pref_short_toggle", false) + set(enabled) { + storage.putBoolean("pref_short_toggle", enabled) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + fun clear() { + storage.clear() + } + + fun setDefaultHabitColor(color: Int) { + storage.putInt("pref_default_habit_palette_color", color) + } + + fun setNotificationsSticky(sticky: Boolean) { + storage.putBoolean("pref_sticky_notifications", sticky) + for (l in listeners) l.onNotificationsChanged() + } + + fun setNotificationsLed(enabled: Boolean) { + storage.putBoolean("pref_led_notifications", enabled) + for (l in listeners) l.onNotificationsChanged() + } + + fun shouldMakeNotificationsSticky(): Boolean { + return storage.getBoolean("pref_sticky_notifications", false) + } + + fun shouldMakeNotificationsLed(): Boolean { + return storage.getBoolean("pref_led_notifications", false) + } + + var isCheckmarkSequenceReversed: Boolean + get() { + if (shouldReverseCheckmarks == null) shouldReverseCheckmarks = + storage.getBoolean("pref_checkmark_reverse_order", false) + return shouldReverseCheckmarks!! + } + set(reverse) { + shouldReverseCheckmarks = reverse + storage.putBoolean("pref_checkmark_reverse_order", reverse) + for (l in listeners) l.onCheckmarkSequenceChanged() + } + + fun updateLastHint(number: Int, timestamp: Timestamp) { + storage.putInt("last_hint_number", number) + storage.putLong("last_hint_timestamp", timestamp.unixTime) + } + + var lastAppVersion: Int + get() = storage.getInt("last_version", 0) + set(version) { + storage.putInt("last_version", version) + } + var widgetOpacity: Int + get() = storage.getString("pref_widget_opacity", "255").toInt() + set(value) { + storage.putString("pref_widget_opacity", value.toString()) + } + var isSkipEnabled: Boolean + get() = storage.getBoolean("pref_skip_enabled", false) + set(value) { + storage.putBoolean("pref_skip_enabled", value) + } + val syncBaseURL: String + get() = storage.getString("pref_sync_base_url", "") + val syncKey: String + get() = storage.getString("pref_sync_key", "") + val encryptionKey: String + get() = storage.getString("pref_encryption_key", "") + val isSyncEnabled: Boolean + get() = storage.getBoolean("pref_sync_enabled", false) + + fun enableSync(syncKey: String, encKey: String) { + storage.putBoolean("pref_sync_enabled", true) + storage.putString("pref_sync_key", syncKey) + storage.putString("pref_encryption_key", encKey) + for (l in listeners) l.onSyncEnabled() + } + + fun disableSync() { + storage.putBoolean("pref_sync_enabled", false) + storage.putString("pref_sync_key", "") + storage.putString("pref_encryption_key", "") + } + + fun areQuestionMarksEnabled(): Boolean { + return storage.getBoolean("pref_unknown_enabled", false) + } + + /** + * @return An integer representing the first day of the week. Sunday + * corresponds to 1, Monday to 2, and so on, until Saturday, which is + * represented by 7. By default, this is based on the current system locale, + * unless the user changed this in the settings. + */ + @get:Deprecated("") + val firstWeekdayInt: Int + get() { + val weekday = storage.getString("pref_first_weekday", "") + return if (weekday.isEmpty()) getFirstWeekdayNumberAccordingToLocale() else weekday.toInt() + } + val firstWeekday: DayOfWeek + get() { + var weekday = storage.getString("pref_first_weekday", "-1").toInt() + if (weekday < 0) weekday = getFirstWeekdayNumberAccordingToLocale() + return when (weekday) { + 1 -> DayOfWeek.SUNDAY + 2 -> DayOfWeek.MONDAY + 3 -> DayOfWeek.TUESDAY + 4 -> DayOfWeek.WEDNESDAY + 5 -> DayOfWeek.THURSDAY + 6 -> DayOfWeek.FRIDAY + 7 -> DayOfWeek.SATURDAY + else -> throw IllegalArgumentException() + } + } + + interface Listener { + fun onCheckmarkSequenceChanged() {} + fun onNotificationsChanged() {} + fun onSyncEnabled() {} + } + + interface Storage { + fun clear() + fun getBoolean(key: String, defValue: Boolean): Boolean + fun getInt(key: String, defValue: Int): Int + fun getLong(key: String, defValue: Long): Long + fun getString(key: String, defValue: String): String + fun onAttached(preferences: Preferences) + fun putBoolean(key: String, value: Boolean) + fun putInt(key: String, value: Int) + fun putLong(key: String, value: Long) + fun putString(key: String, value: String) + fun remove(key: String) + fun putLongArray(key: String, values: LongArray) { + putString(key, joinLongs(values)) + } + + fun getLongArray(key: String, defValue: LongArray): LongArray? { + val string = getString(key, "") + return if (string.isEmpty()) defValue else splitLongs( + string + ) + } + } + + init { + listeners = LinkedList() + storage.onAttached(this) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.java deleted file mode 100644 index a1e231b4c..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.preferences; - -import androidx.annotation.*; - -import java.io.*; -import java.util.*; - -public class PropertiesStorage implements Preferences.Storage -{ - @NonNull - private final Properties props; - - @NonNull - private File file; - - public PropertiesStorage(@NonNull File file) - { - try - { - this.file = file; - props = new Properties(); - props.load(new FileInputStream(file)); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - @Override - public void clear() - { - for(String key : props.stringPropertyNames()) props.remove(key); - flush(); - } - - @Override - public boolean getBoolean(String key, boolean defValue) - { - String value = props.getProperty(key, Boolean.toString(defValue)); - return Boolean.parseBoolean(value); - } - - @Override - public int getInt(String key, int defValue) - { - String value = props.getProperty(key, Integer.toString(defValue)); - return Integer.parseInt(value); - } - - @Override - public long getLong(String key, long defValue) - { - String value = props.getProperty(key, Long.toString(defValue)); - return Long.parseLong(value); - } - - @Override - public String getString(String key, String defValue) - { - return props.getProperty(key, defValue); - } - - @Override - public void onAttached(Preferences preferences) - { - // nop - } - - @Override - public void putBoolean(String key, boolean value) - { - props.setProperty(key, Boolean.toString(value)); - } - - @Override - public void putInt(String key, int value) - { - props.setProperty(key, Integer.toString(value)); - flush(); - } - - private void flush() - { - try - { - props.store(new FileOutputStream(file), ""); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - @Override - public void putLong(String key, long value) - { - props.setProperty(key, Long.toString(value)); - flush(); - } - - @Override - public void putString(String key, String value) - { - props.setProperty(key, value); - flush(); - } - - @Override - public void remove(String key) - { - props.remove(key); - flush(); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.kt new file mode 100644 index 000000000..8987730e6 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/PropertiesStorage.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.preferences + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.Properties + +class PropertiesStorage(file: File) : Preferences.Storage { + private var props: Properties + private val file: File + override fun clear() { + for (key in props.stringPropertyNames()) props.remove(key) + flush() + } + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + val value = props.getProperty(key, java.lang.Boolean.toString(defValue)) + return java.lang.Boolean.parseBoolean(value) + } + + override fun getInt(key: String, defValue: Int): Int { + val value = props.getProperty(key, defValue.toString()) + return value.toInt() + } + + override fun getLong(key: String, defValue: Long): Long { + val value = props.getProperty(key, defValue.toString()) + return value.toLong() + } + + override fun getString(key: String, defValue: String): String { + return props.getProperty(key, defValue) + } + + override fun onAttached(preferences: Preferences) { + // nop + } + + override fun putBoolean(key: String, value: Boolean) { + props.setProperty(key, java.lang.Boolean.toString(value)) + } + + override fun putInt(key: String, value: Int) { + props.setProperty(key, value.toString()) + flush() + } + + private fun flush() { + try { + props.store(FileOutputStream(file), "") + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun putLong(key: String, value: Long) { + props.setProperty(key, value.toString()) + flush() + } + + override fun putString(key: String, value: String) { + props.setProperty(key, value) + flush() + } + + override fun remove(key: String) { + props.remove(key) + flush() + } + + init { + try { + this.file = file + props = Properties() + props.load(FileInputStream(file)) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java deleted file mode 100644 index b75316abe..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.preferences; - -import org.isoron.uhabits.core.AppScope; - -import javax.inject.Inject; - -@AppScope -public class WidgetPreferences { - private Preferences.Storage storage; - - @Inject - public WidgetPreferences(Preferences.Storage storage) { - this.storage = storage; - } - - public void addWidget(int widgetId, long[] habitIds) { - storage.putLongArray(getHabitIdKey(widgetId), habitIds); - } - - public long[] getHabitIdsFromWidgetId(int widgetId) { - long[] habitIds; - String habitIdKey = getHabitIdKey(widgetId); - try { - habitIds = storage.getLongArray(habitIdKey, new long[]{-1}); - } catch (ClassCastException e) { - // Up to Loop 1.7.11, this preference was not an array, but a single - // long. Trying to read the old preference causes a cast exception. - habitIds = new long[1]; - habitIds[0] = storage.getLong(habitIdKey, -1); - storage.putLongArray(habitIdKey, habitIds); - } - return habitIds; - } - - public void removeWidget(int id) { - String habitIdKey = getHabitIdKey(id); - storage.remove(habitIdKey); - } - - public long getSnoozeTime(long id) - { - return storage.getLong(getSnoozeKey(id), 0); - } - - private String getHabitIdKey(int id) { - return String.format("widget-%06d-habit", id); - } - - private String getSnoozeKey(long id) - { - return String.format("snooze-%06d", id); - } - - public void removeSnoozeTime(long id) - { - storage.putLong(getSnoozeKey(id), 0); - } - - public void setSnoozeTime(Long id, long time) - { - storage.putLong(getSnoozeKey(id), time); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.kt new file mode 100644 index 000000000..68bff6885 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/WidgetPreferences.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.preferences + +import org.isoron.uhabits.core.AppScope +import javax.inject.Inject + +@AppScope +class WidgetPreferences @Inject constructor(private val storage: Preferences.Storage) { + fun addWidget(widgetId: Int, habitIds: LongArray) { + storage.putLongArray(getHabitIdKey(widgetId), habitIds) + } + + fun getHabitIdsFromWidgetId(widgetId: Int): LongArray? { + var habitIds: LongArray? + val habitIdKey = getHabitIdKey(widgetId) + try { + habitIds = storage.getLongArray(habitIdKey, longArrayOf(-1)) + } catch (e: ClassCastException) { + // Up to Loop 1.7.11, this preference was not an array, but a single + // long. Trying to read the old preference causes a cast exception. + habitIds = LongArray(1) + habitIds[0] = storage.getLong(habitIdKey, -1) + storage.putLongArray(habitIdKey, habitIds) + } + return habitIds + } + + fun removeWidget(id: Int) { + val habitIdKey = getHabitIdKey(id) + storage.remove(habitIdKey) + } + + fun getSnoozeTime(id: Long): Long { + return storage.getLong(getSnoozeKey(id), 0) + } + + private fun getHabitIdKey(id: Int): String { + return String.format("widget-%06d-habit", id) + } + + private fun getSnoozeKey(id: Long): String { + return String.format("snooze-%06d", id) + } + + fun removeSnoozeTime(id: Long) { + storage.putLong(getSnoozeKey(id), 0) + } + + fun setSnoozeTime(id: Long, time: Long) { + storage.putLong(getSnoozeKey(id), time) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt index 7f602e11a..1b40d30da 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt @@ -129,7 +129,7 @@ class ReminderScheduler @Inject constructor( fun snoozeReminder(habit: Habit, minutes: Long) { val now = applyTimezone(getLocalTime()) val snoozedUntil = now + minutes * 60 * 1000 - widgetPreferences.setSnoozeTime(habit.id, snoozedUntil) + widgetPreferences.setSnoozeTime(habit.id!!, snoozedUntil) schedule(habit) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt index 8ef33a051..fec0d4099 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt @@ -51,6 +51,6 @@ class HintList(private val prefs: Preferences, private val hints: Array) fun shouldShow(): Boolean { val today = getToday() val lastHintTimestamp = prefs.lastHintTimestamp - return lastHintTimestamp.isOlderThan(today) + return lastHintTimestamp?.isOlderThan(today) == true } } From 457c58a660171de209c2e9258d7a94bb5e29ce72 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 18:49:26 +0100 Subject: [PATCH 10/35] Convert RepositoryTest --- .../uhabits/HabitsActivityTestComponent.kt | 5 +- .../habits/list/views/HeaderViewTest.java | 80 ------- .../habits/list/views/HeaderViewTest.kt | 68 ++++++ .../habits/list/views/HintViewTest.java | 80 ------- .../habits/list/views/HintViewTest.kt | 72 +++++++ .../activities/common/views/StreakChart.java | 36 ++-- .../habits/list/views/HabitCardListAdapter.kt | 2 +- .../uhabits/core/database/Repository.kt | 1 + .../uhabits/core/preferences/Preferences.kt | 6 +- .../core/ui/screens/habits/list/HintList.kt | 6 +- .../screens/habits/list/ListHabitsBehavior.kt | 2 +- .../uhabits/core/database/RepositoryTest.java | 198 ------------------ .../uhabits/core/database/RepositoryTest.kt | 156 ++++++++++++++ .../uhabits/core/database/ThingRecord.kt | 46 ++++ 14 files changed, 376 insertions(+), 382 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt delete mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.java create mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt create mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt index 9dade3a8c..a7a1b2759 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt @@ -19,6 +19,7 @@ package org.isoron.uhabits +import com.nhaarman.mockitokotlin2.mock import dagger.Component import dagger.Module import dagger.Provides @@ -34,11 +35,11 @@ import org.isoron.uhabits.inject.ActivityScope import org.isoron.uhabits.inject.HabitModule import org.isoron.uhabits.inject.HabitsActivityModule import org.isoron.uhabits.inject.HabitsApplicationComponent -import org.mockito.Mockito.mock @Module class TestModule { - @Provides fun listHabitsBehavior(): ListHabitsBehavior = mock(ListHabitsBehavior::class.java) + @Provides + fun listHabitsBehavior(): ListHabitsBehavior = mock() } @ActivityScope diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.java deleted file mode 100644 index c4246c615..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.habits.list.views; - -import androidx.test.filters.*; -import androidx.test.runner.*; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.utils.*; -import org.junit.*; -import org.junit.runner.*; - -import static org.mockito.Mockito.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class HeaderViewTest extends BaseViewTest -{ - public static final String PATH = "habits/list/HeaderView/"; - - private HeaderView view; - - private Preferences prefs; - - private MidnightTimer midnightTimer; - - @Override - @Before - public void setUp() - { - super.setUp(); - prefs = mock(Preferences.class); - midnightTimer = mock(MidnightTimer.class); - view = new HeaderView(targetContext, prefs, midnightTimer); - view.setButtonCount(5); - measureView(view, dpToPixels(600), dpToPixels(48)); - } - - @Test - public void testRender() throws Exception - { - when(prefs.isCheckmarkSequenceReversed()).thenReturn(false); - - assertRenders(view, PATH + "render.png"); - - verify(prefs).isCheckmarkSequenceReversed(); - verifyNoMoreInteractions(prefs); - } - - @Test - public void testRender_reverse() throws Exception - { - when(prefs.isCheckmarkSequenceReversed()).thenReturn(true); - - assertRenders(view, PATH + "render_reverse.png"); - - verify(prefs).isCheckmarkSequenceReversed(); - verifyNoMoreInteractions(prefs); - } -} \ No newline at end of file diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt new file mode 100644 index 000000000..ccfebd949 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.habits.list.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.isoron.uhabits.BaseViewTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito + +@RunWith(AndroidJUnit4::class) +@MediumTest +class HeaderViewTest : BaseViewTest() { + private var view: HeaderView? = null + + @Before + override fun setUp() { + super.setUp() + prefs = mock() + view = HeaderView(targetContext, prefs, mock()) + view!!.buttonCount = 5 + measureView(view, dpToPixels(600), dpToPixels(48)) + } + + @Test + @Throws(Exception::class) + fun testRender() { + Mockito.`when`(prefs.isCheckmarkSequenceReversed).thenReturn(false) + assertRenders(view, PATH + "render.png") + Mockito.verify(prefs).isCheckmarkSequenceReversed + Mockito.verifyNoMoreInteractions(prefs) + } + + @Test + @Throws(Exception::class) + fun testRender_reverse() { + doReturn(true).whenever(prefs).isCheckmarkSequenceReversed + // Mockito.`when`(prefs.isCheckmarkSequenceReversed).thenReturn(true) + assertRenders(view, PATH + "render_reverse.png") + Mockito.verify(prefs).isCheckmarkSequenceReversed + Mockito.verifyNoMoreInteractions(prefs) + } + + companion object { + const val PATH = "habits/list/HeaderView/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java deleted file mode 100644 index 09291948f..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.habits.list.views; - -import androidx.test.filters.*; -import androidx.test.runner.*; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.ui.screens.habits.list.*; -import org.junit.*; -import org.junit.runner.*; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.*; -import static org.mockito.Mockito.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class HintViewTest extends BaseViewTest -{ - public static final String PATH = "habits/list/HintView/"; - - private HintView view; - - private HintList list; - - @Before - @Override - public void setUp() - { - super.setUp(); - - list = mock(HintList.class); - view = new HintView(targetContext, list); - measureView(view, 400, 200); - - String text = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - - when(list.shouldShow()).thenReturn(true); - when(list.pop()).thenReturn(text); - - view.showNext(); - skipAnimation(view); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } - - @Test - public void testClick() throws Exception - { - assertThat(view.getAlpha(), equalTo(1f)); - view.performClick(); - skipAnimation(view); - assertThat(view.getAlpha(), equalTo(0f)); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt new file mode 100644 index 000000000..6685acc9e --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.habits.list.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.core.ui.screens.habits.list.HintList +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class HintViewTest : BaseViewTest() { + private lateinit var view: HintView + private lateinit var list: HintList + @Before + override fun setUp() { + super.setUp() + list = mock() + view = HintView(targetContext, list) + measureView(view, 400f, 200f) + val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + doReturn(true).whenever(list).shouldShow() + doReturn(text).whenever(list).pop() + // Mockito.`when`(list.shouldShow()).thenReturn(true) + // Mockito.`when`(list.pop()).thenReturn(text) + view.showNext() + skipAnimation(view) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + @Test + @Throws(Exception::class) + fun testClick() { + MatcherAssert.assertThat(view.alpha, CoreMatchers.equalTo(1f)) + view.performClick() + skipAnimation(view) + MatcherAssert.assertThat(view.alpha, CoreMatchers.equalTo(0f)) + } + + companion object { + const val PATH = "habits/list/HintView/" + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java index 16961c335..1ebf43202 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java @@ -19,19 +19,27 @@ package org.isoron.uhabits.activities.common.views; -import android.content.*; -import android.graphics.*; -import android.util.*; -import android.view.*; -import android.view.ViewGroup.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; - -import java.text.*; -import java.util.*; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.core.models.Streak; +import org.isoron.uhabits.core.models.Timestamp; +import org.isoron.uhabits.core.utils.DateUtils; +import org.isoron.uhabits.utils.StyledResources; + +import java.text.DateFormat; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.TimeZone; import static android.view.View.MeasureSpec.*; import static org.isoron.uhabits.utils.InterfaceUtils.*; @@ -98,7 +106,7 @@ public class StreakChart extends View public void populateWithRandomData() { Timestamp start = DateUtils.getToday(); - LinkedList streaks = new LinkedList<>(); + List streaks = new LinkedList<>(); for (int i = 0; i < 10; i++) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index c968a98db..8e3a7d96d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -121,7 +121,7 @@ class HabitCardListAdapter @Inject constructor( val score = cache.getScore(habit!!.id!!) val checkmarks = cache.getCheckmarks(habit.id!!) val selected = selected.contains(habit) - listView!!.bindCardView(holder, habit, score, checkmarks!!, selected) + listView!!.bindCardView(holder, habit, score, checkmarks, selected) } override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/database/Repository.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/database/Repository.kt index 3f8a76995..7fc3a6142 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/database/Repository.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/database/Repository.kt @@ -168,6 +168,7 @@ class Repository( get() { val fields: MutableList> = ArrayList() for (f in klass.declaredFields) { + f.isAccessible = true for (annotation in f.annotations) { if (annotation !is Column) continue fields.add(ImmutablePair(f, annotation)) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index 43fd47a24..3923cf87a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -29,7 +29,7 @@ import java.util.LinkedList import kotlin.math.max import kotlin.math.min -class Preferences(private val storage: Storage) { +open class Preferences(private val storage: Storage) { private val listeners: MutableList private var shouldReverseCheckmarks: Boolean? = null fun addListener(listener: Listener) { @@ -86,7 +86,7 @@ class Preferences(private val storage: Storage) { } val lastHintNumber: Int get() = storage.getInt("last_hint_number", -1) - val lastHintTimestamp: Timestamp? + open val lastHintTimestamp: Timestamp? get() { val unixTime = storage.getLong("last_hint_timestamp", -1) return if (unixTime < 0) null else Timestamp(unixTime) @@ -171,7 +171,7 @@ class Preferences(private val storage: Storage) { return storage.getBoolean("pref_led_notifications", false) } - var isCheckmarkSequenceReversed: Boolean + open var isCheckmarkSequenceReversed: Boolean get() { if (shouldReverseCheckmarks == null) shouldReverseCheckmarks = storage.getBoolean("pref_checkmark_reverse_order", false) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt index fec0d4099..f15f4fced 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt @@ -25,7 +25,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday * Provides a list of hints to be shown at the application startup, and takes * care of deciding when a new hint should be shown. */ -class HintList(private val prefs: Preferences, private val hints: Array) { +open class HintList(private val prefs: Preferences, private val hints: Array) { /** * Returns a new hint to be shown to the user. * @@ -36,7 +36,7 @@ class HintList(private val prefs: Preferences, private val hints: Array) * * @return the next hint to be shown, or null if none */ - fun pop(): String? { + open fun pop(): String? { val next = prefs.lastHintNumber + 1 if (next >= hints.size) return null prefs.updateLastHint(next, getToday()) @@ -48,7 +48,7 @@ class HintList(private val prefs: Preferences, private val hints: Array) * * @return true if hint should be shown, false otherwise */ - fun shouldShow(): Boolean { + open fun shouldShow(): Boolean { val today = getToday() val lastHintTimestamp = prefs.lastHintTimestamp return lastHintTimestamp?.isOlderThan(today) == true diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 8144a8fb3..0c52a8dcd 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -33,7 +33,7 @@ import java.io.IOException import java.util.LinkedList import javax.inject.Inject -class ListHabitsBehavior @Inject constructor( +open class ListHabitsBehavior @Inject constructor( private val habitList: HabitList, private val dirFinder: DirFinder, private val taskRunner: TaskRunner, diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.java b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.java deleted file mode 100644 index 232ad25ff..000000000 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.database; - -import org.apache.commons.lang3.builder.*; -import org.isoron.uhabits.core.*; -import org.junit.*; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - - -public class RepositoryTest extends BaseUnitTest -{ - private Repository repository; - - private Database db; - - @Before - @Override - public void setUp() throws Exception - { - super.setUp(); - this.db = BaseUnitTest.Companion.buildMemoryDatabase(); - repository = new Repository<>(ThingRecord.class, db); - - db.execute("drop table if exists tests"); - db.execute("create table tests(" + - "id integer not null primary key autoincrement, " + - "color_number integer not null, score float not null, " + - "name string)"); - } - - @Test - public void testFind() throws Exception - { - db.execute("insert into tests(id, color_number, name, score) " + - "values (10, 20, 'hello', 8.0)"); - - ThingRecord record = repository.find(10L); - - assertNotNull(record); - assertThat(record.id, equalTo(10L)); - assertThat(record.color, equalTo(20)); - assertThat(record.name, equalTo("hello")); - assertThat(record.score, equalTo(8.0)); - } - - @Test - public void testSave_withId() throws Exception - { - ThingRecord record = new ThingRecord(); - record.id = 50L; - record.color = 10; - record.name = "hello"; - record.score = 5.0; - repository.save(record); - assertThat(record, equalTo(repository.find(50L))); - - record.name = "world"; - record.score = 128.0; - repository.save(record); - assertThat(record, equalTo(repository.find(50L))); - } - - @Test - public void testSave_withNull() throws Exception - { - ThingRecord record = new ThingRecord(); - record.color = 50; - record.name = null; - record.score = 12.0; - repository.save(record); - - ThingRecord retrieved = repository.find(record.id); - assertNotNull(retrieved); - assertNull(retrieved.name); - assertThat(record, equalTo(retrieved)); - } - - @Test - public void testSave_withoutId() throws Exception - { - ThingRecord r1 = new ThingRecord(); - r1.color = 10; - r1.name = "hello"; - r1.score = 16.0; - repository.save(r1); - - ThingRecord r2 = new ThingRecord(); - r2.color = 20; - r2.name = "world"; - r2.score = 2.0; - repository.save(r2); - - assertThat(r1.id, equalTo(1L)); - assertThat(r2.id, equalTo(2L)); - } - - @Test - public void testRemove() throws Exception - { - ThingRecord rec1 = new ThingRecord(); - rec1.color = 10; - rec1.name = "hello"; - rec1.score = 16.0; - repository.save(rec1); - - ThingRecord rec2 = new ThingRecord(); - rec2.color = 20; - rec2.name = "world"; - rec2.score = 32.0; - repository.save(rec2); - - long id = rec1.id; - assertThat(rec1, equalTo(repository.find(id))); - assertThat(rec2, equalTo(repository.find(rec2.id))); - - repository.remove(rec1); - assertThat(rec1.id, equalTo(null)); - assertNull(repository.find(id)); - assertThat(rec2, equalTo(repository.find(rec2.id))); - - repository.remove(rec1); // should have no effect - assertNull(repository.find(id)); - } -} - -@Table(name = "tests") -class ThingRecord -{ - @Column - public Long id; - - @Column - public String name; - - @Column(name = "color_number") - public Integer color; - - @Column - public Double score; - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - - if (o == null || getClass() != o.getClass()) return false; - - ThingRecord record = (ThingRecord) o; - - return new EqualsBuilder() - .append(id, record.id) - .append(name, record.name) - .append(color, record.color) - .isEquals(); - } - - @Override - public int hashCode() - { - return new HashCodeBuilder(17, 37) - .append(id) - .append(name) - .append(color) - .toHashCode(); - } - - @Override - public String toString() - { - return new ToStringBuilder(this) - .append("id", id) - .append("name", name) - .append("color", color) - .toString(); - } -} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt new file mode 100644 index 000000000..1d13b98a0 --- /dev/null +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.database + +import org.hamcrest.MatcherAssert +import org.hamcrest.core.IsEqual +import org.isoron.uhabits.core.BaseUnitTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class RepositoryTest : BaseUnitTest() { + private var repository: Repository? = null + private var db: Database? = null + @Before + @Throws(Exception::class) + override fun setUp() { + super.setUp() + db = buildMemoryDatabase() + repository = Repository(ThingRecord::class.java, db!!) + db!!.execute("drop table if exists tests") + db!!.execute( + "create table tests(" + + "id integer not null primary key autoincrement, " + + "color_number integer not null, score float not null, " + + "name string)" + ) + } + + @Test + @Throws(Exception::class) + fun testFind() { + db!!.execute( + "insert into tests(id, color_number, name, score) " + + "values (10, 20, 'hello', 8.0)" + ) + val record = repository!!.find(10L) + Assert.assertNotNull(record) + MatcherAssert.assertThat(record!!.id, IsEqual.equalTo(10L)) + MatcherAssert.assertThat(record.color, IsEqual.equalTo(20)) + MatcherAssert.assertThat(record.name, IsEqual.equalTo("hello")) + MatcherAssert.assertThat(record.score, IsEqual.equalTo(8.0)) + } + + @Test + @Throws(Exception::class) + fun testSave_withId() { + val record = ThingRecord() + record.id = 50L + record.color = 10 + record.name = "hello" + record.score = 5.0 + repository!!.save(record) + MatcherAssert.assertThat( + record, + IsEqual.equalTo( + repository!!.find(50L) + ) + ) + record.name = "world" + record.score = 128.0 + repository!!.save(record) + MatcherAssert.assertThat( + record, + IsEqual.equalTo( + repository!!.find(50L) + ) + ) + } + + @Test + @Throws(Exception::class) + fun testSave_withNull() { + val record = ThingRecord() + record.color = 50 + record.name = null + record.score = 12.0 + repository!!.save(record) + val retrieved = repository!!.find(record.id!!) + Assert.assertNotNull(retrieved) + Assert.assertNull(retrieved!!.name) + MatcherAssert.assertThat(record, IsEqual.equalTo(retrieved)) + } + + @Test + @Throws(Exception::class) + fun testSave_withoutId() { + val r1 = ThingRecord() + r1.color = 10 + r1.name = "hello" + r1.score = 16.0 + repository!!.save(r1) + val r2 = ThingRecord() + r2.color = 20 + r2.name = "world" + r2.score = 2.0 + repository!!.save(r2) + MatcherAssert.assertThat(r1.id, IsEqual.equalTo(1L)) + MatcherAssert.assertThat(r2.id, IsEqual.equalTo(2L)) + } + + @Test + @Throws(Exception::class) + fun testRemove() { + val rec1 = ThingRecord() + rec1.color = 10 + rec1.name = "hello" + rec1.score = 16.0 + repository!!.save(rec1) + val rec2 = ThingRecord() + rec2.color = 20 + rec2.name = "world" + rec2.score = 32.0 + repository!!.save(rec2) + val id = rec1.id!! + MatcherAssert.assertThat( + rec1, + IsEqual.equalTo( + repository!!.find(id) + ) + ) + MatcherAssert.assertThat( + rec2, + IsEqual.equalTo( + repository!!.find(rec2.id!!) + ) + ) + repository!!.remove(rec1) + MatcherAssert.assertThat(rec1.id, IsEqual.equalTo(null)) + Assert.assertNull(repository!!.find(id)) + MatcherAssert.assertThat( + rec2, + IsEqual.equalTo( + repository!!.find(rec2.id!!) + ) + ) + repository!!.remove(rec1) // should have no effect + Assert.assertNull(repository!!.find(id)) + } +} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt new file mode 100644 index 000000000..47c4eaaba --- /dev/null +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt @@ -0,0 +1,46 @@ +package org.isoron.uhabits.core.database + +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.apache.commons.lang3.builder.ToStringBuilder + +@Table(name = "tests") +open class ThingRecord { + @field:Column + open var id: Long? = null + + @field:Column + open var name: String? = null + + @field:Column(name = "color_number") + open var color: Int? = null + + @field:Column + open var score: Double? = null + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val record = other as ThingRecord + return EqualsBuilder() + .append(id, record.id) + .append(name, record.name) + .append(color, record.color) + .isEquals + } + + override fun hashCode(): Int { + return HashCodeBuilder(17, 37) + .append(id) + .append(name) + .append(color) + .toHashCode() + } + + override fun toString(): String { + return ToStringBuilder(this) + .append("id", id) + .append("name", name) + .append("color", color) + .toString() + } +} From 228be95f9c104e105161ac01afa2daa7e2da118e Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 22:00:35 +0100 Subject: [PATCH 11/35] convert widget tests --- .../uhabits/widgets/CheckmarkWidgetTest.java | 95 ------------------- .../uhabits/widgets/CheckmarkWidgetTest.kt | 89 +++++++++++++++++ .../uhabits/widgets/FrequencyWidgetTest.java | 67 ------------- .../uhabits/widgets/FrequencyWidgetTest.kt | 59 ++++++++++++ .../uhabits/widgets/HistoryWidgetTest.java | 65 ------------- .../uhabits/widgets/HistoryWidgetTest.kt | 58 +++++++++++ .../uhabits/widgets/ScoreWidgetTest.java | 65 ------------- .../isoron/uhabits/widgets/ScoreWidgetTest.kt | 58 +++++++++++ .../uhabits/widgets/StreakWidgetTest.java | 65 ------------- .../uhabits/widgets/StreakWidgetTest.kt | 58 +++++++++++ .../uhabits/widgets/TargetWidgetTest.java | 68 ------------- .../uhabits/widgets/TargetWidgetTest.kt | 63 ++++++++++++ .../views/CheckmarkWidgetViewTest.java | 79 --------------- .../widgets/views/CheckmarkWidgetViewTest.kt | 73 ++++++++++++++ 14 files changed, 458 insertions(+), 504 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java deleted file mode 100644 index a76efadec..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.junit.*; -import org.junit.runner.*; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.*; -import static org.isoron.uhabits.core.models.Entry.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class CheckmarkWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/CheckmarkWidget/"; - - private Habit habit; - - private EntryList entries; - - private FrameLayout view; - - private Timestamp today = DateUtils.getTodayWithOffset(); - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - prefs.setSkipEnabled(true); - - habit = fixtures.createVeryLongHabit(); - entries = habit.getComputedEntries(); - CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit); - view = convertToView(widget, 150, 200); - - assertThat(entries.get(today).getValue(), equalTo(YES_MANUAL)); - } - - @Test - public void testClick() throws Exception - { - Button button = (Button) view.findViewById(R.id.button); - assertThat(button, is(not(nullValue()))); - - // A better test would be to capture the intent, but it doesn't seem - // possible to capture intents sent to BroadcastReceivers. - button.performClick(); - sleep(1000); - assertThat(entries.get(today).getValue(), equalTo(SKIP)); - - button.performClick(); - sleep(1000); - assertThat(entries.get(today).getValue(), equalTo(NO)); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(CheckmarkWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt new file mode 100644 index 000000000..0a5823bbf --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.view.View +import android.widget.Button +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.models.EntryList +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class CheckmarkWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var entries: EntryList + private lateinit var view: FrameLayout + private val today = getTodayWithOffset() + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + prefs.isSkipEnabled = true + habit = fixtures.createVeryLongHabit() + entries = habit.computedEntries + val widget = CheckmarkWidget(targetContext, 0, habit) + view = convertToView(widget, 150, 200) + MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.YES_MANUAL)) + } + + @Test + @Throws(Exception::class) + fun testClick() { + val button = view.findViewById(R.id.button) as Button + MatcherAssert.assertThat( + button, + CoreMatchers.`is`(CoreMatchers.not(CoreMatchers.nullValue())) + ) + + // A better test would be to capture the intent, but it doesn't seem + // possible to capture intents sent to BroadcastReceivers. + button.performClick() + sleep(1000) + MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.SKIP)) + button.performClick() + sleep(1000) + MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.NO)) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(CheckmarkWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/CheckmarkWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.java deleted file mode 100644 index 29f0befda..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; -import org.junit.runner.*; - -import java.util.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class FrequencyWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/FrequencyWidget/"; - - private Habit habit; - - private FrameLayout view; - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - - habit = fixtures.createVeryLongHabit(); - FrequencyWidget widget = new FrequencyWidget(targetContext, 0, habit, Calendar.SUNDAY); - view = convertToView(widget, 400, 400); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(FrequencyWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt new file mode 100644 index 000000000..721aefdf7 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Habit +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Calendar + +@RunWith(AndroidJUnit4::class) +@MediumTest +class FrequencyWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var view: FrameLayout + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + habit = fixtures.createVeryLongHabit() + val widget = FrequencyWidget(targetContext, 0, habit, Calendar.SUNDAY) + view = convertToView(widget, 400, 400) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(FrequencyWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/FrequencyWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.java deleted file mode 100644 index ea9c60ea9..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class HistoryWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/HistoryWidget/"; - - private Habit habit; - - private FrameLayout view; - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - - habit = fixtures.createVeryLongHabit(); - HistoryWidget widget = new HistoryWidget(targetContext, 0, habit); - view = convertToView(widget, 400, 400); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(HistoryWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.kt new file mode 100644 index 000000000..d75ca3219 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Habit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class HistoryWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var view: FrameLayout + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + habit = fixtures.createVeryLongHabit() + val widget = HistoryWidget(targetContext, 0, habit) + view = convertToView(widget, 400, 400) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(HistoryWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/HistoryWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java deleted file mode 100644 index 74516a754..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class ScoreWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/ScoreWidget/"; - - private Habit habit; - - private FrameLayout view; - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - - habit = fixtures.createVeryLongHabit(); - ScoreWidget widget = new ScoreWidget(targetContext, 0, habit); - view = convertToView(widget, 400, 400); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(ScoreWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.kt new file mode 100644 index 000000000..88826399e --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Habit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class ScoreWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var view: FrameLayout + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + habit = fixtures.createVeryLongHabit() + val widget = ScoreWidget(targetContext, 0, habit) + view = convertToView(widget, 400, 400) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(ScoreWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/ScoreWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java deleted file mode 100644 index 8be405687..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class StreakWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/StreakWidget/"; - - private Habit habit; - - private FrameLayout view; - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - - habit = fixtures.createVeryLongHabit(); - StreakWidget widget = new StreakWidget(targetContext, 0, habit); - view = convertToView(widget, 400, 400); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(StreakWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.kt new file mode 100644 index 000000000..9bfe2742f --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Habit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class StreakWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var view: FrameLayout + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + habit = fixtures.createVeryLongHabit() + val widget = StreakWidget(targetContext, 0, habit) + view = convertToView(widget, 400, 400) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(StreakWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/StreakWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.java deleted file mode 100644 index d4109c3fb..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.widget.*; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class TargetWidgetTest extends BaseViewTest -{ - private static final String PATH = "widgets/TargetWidget/"; - - private Habit habit; - - private FrameLayout view; - - @Override - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - prefs.setWidgetOpacity(255); - - habit = fixtures.createLongNumericalHabit(); - habit.setColor(new PaletteColor(11)); - habit.setFrequency(Frequency.WEEKLY); - habit.recompute(); - TargetWidget widget = new TargetWidget(targetContext, 0, habit); - view = convertToView(widget, 400, 400); - } - - @Test - public void testIsInstalled() - { - assertWidgetProviderIsInstalled(TargetWidgetProvider.class); - } - - @Test - public void testRender() throws Exception - { - assertRenders(view, PATH + "render.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt new file mode 100644 index 000000000..fd49a0eb1 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets + +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Frequency +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.PaletteColor +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class TargetWidgetTest : BaseViewTest() { + private lateinit var habit: Habit + private lateinit var view: FrameLayout + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + prefs.widgetOpacity = 255 + habit = fixtures.createLongNumericalHabit() + habit.color = PaletteColor(11) + habit.frequency = Frequency.WEEKLY + habit.recompute() + val widget = TargetWidget(targetContext, 0, habit) + view = convertToView(widget, 400, 400) + } + + @Test + fun testIsInstalled() { + assertWidgetProviderIsInstalled(TargetWidgetProvider::class.java) + } + + @Test + @Throws(Exception::class) + fun testRender() { + assertRenders(view, PATH + "render.png") + } + + companion object { + private const val PATH = "widgets/TargetWidget/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java deleted file mode 100644 index 05a5e3265..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets.views; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; -import org.junit.runner.*; - -import java.io.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class CheckmarkWidgetViewTest extends BaseViewTest -{ - private static final String PATH = "widgets/CheckmarkWidgetView/"; - - private CheckmarkWidgetView view; - - @Override - @Before - public void setUp() - { - super.setUp(); - setTheme(R.style.WidgetTheme); - - Habit habit = fixtures.createShortHabit(); - Timestamp today = DateUtils.getTodayWithOffset(); - - view = new CheckmarkWidgetView(targetContext); - double score = habit.getScores().get(today).getValue(); - float percentage = (float) score; - - view.setActiveColor(PaletteUtils.getAndroidTestColor(0)); - view.setEntryState(habit.getComputedEntries().get(today).getValue()); - view.setEntryValue(habit.getComputedEntries().get(today).getValue()); - view.setPercentage(percentage); - view.setName(habit.getName()); - view.refresh(); - measureView(view, dpToPixels(100), dpToPixels(200)); - } - - @Test - public void testRender_checked() throws IOException - { - assertRenders(view, PATH + "checked.png"); - } - - - @Test - public void testRender_largeSize() throws IOException - { - measureView(view, dpToPixels(300), dpToPixels(300)); - assertRenders(view, PATH + "large_size.png"); - } - -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt new file mode 100644 index 000000000..9cd494152 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.widgets.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.R +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +@MediumTest +class CheckmarkWidgetViewTest : BaseViewTest() { + private var view: CheckmarkWidgetView? = null + @Before + override fun setUp() { + super.setUp() + setTheme(R.style.WidgetTheme) + val habit = fixtures.createShortHabit() + val name = habit.name + val computedEntries = habit.computedEntries + val scores = habit.scores + val today = getTodayWithOffset() + view = CheckmarkWidgetView(targetContext) + val score = scores[today].value + val percentage = score.toFloat() + view!!.activeColor = getAndroidTestColor(0) + view!!.entryState = computedEntries.get(today).value + view!!.entryValue = computedEntries.get(today).value + view!!.percentage = percentage + view!!.name = name + view!!.refresh() + measureView(view, dpToPixels(100), dpToPixels(200)) + } + + @Test + @Throws(IOException::class) + fun testRender_checked() { + assertRenders(view, PATH + "checked.png") + } + + @Test + @Throws(IOException::class) + fun testRender_largeSize() { + measureView(view, dpToPixels(300), dpToPixels(300)) + assertRenders(view, PATH + "large_size.png") + } + + companion object { + private const val PATH = "widgets/CheckmarkWidgetView/" + } +} From 98f9693cffe0ce540490a9afcda8b319817fcbc5 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 22:30:37 +0100 Subject: [PATCH 12/35] Convert views --- .../common/views/BundleSavedState.java | 71 --- .../common/views/BundleSavedState.kt | 62 +++ .../common/views/FrequencyChart.java | 347 -------------- .../activities/common/views/FrequencyChart.kt | 291 +++++++++++ .../views/{HabitChart.java => HabitChart.kt} | 13 +- .../activities/common/views/RingView.java | 272 ----------- .../activities/common/views/RingView.kt | 211 ++++++++ .../activities/common/views/ScoreChart.java | 452 ------------------ .../activities/common/views/ScoreChart.kt | 375 +++++++++++++++ .../common/views/ScrollableChart.java | 245 ---------- .../common/views/ScrollableChart.kt | 196 ++++++++ .../activities/common/views/StreakChart.java | 321 ------------- .../activities/common/views/StreakChart.kt | 246 ++++++++++ .../activities/common/views/TargetChart.java | 226 --------- .../activities/common/views/TargetChart.kt | 186 +++++++ .../habits/list/views/HabitCardListView.kt | 2 +- .../habits/list/views/HabitCardView.kt | 11 +- .../habits/show/views/OverviewCardView.kt | 7 +- .../habits/show/views/ScoreCardView.kt | 2 +- .../org/isoron/uhabits/widgets/ScoreWidget.kt | 2 +- .../widgets/views/CheckmarkWidgetView.kt | 6 +- 21 files changed, 1591 insertions(+), 1953 deletions(-) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt rename uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/{HabitChart.java => HabitChart.kt} (80%) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java deleted file mode 100644 index 516a0a80b..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.os.*; - -import androidx.customview.view.*; - -public class BundleSavedState extends AbsSavedState -{ - public static final Parcelable.Creator CREATOR = - new ClassLoaderCreator() - { - @Override - public BundleSavedState createFromParcel(Parcel source, - ClassLoader loader) - { - return new BundleSavedState(source, loader); - } - - @Override - public BundleSavedState createFromParcel(Parcel source) - { - return null; - } - - @Override - public BundleSavedState[] newArray(int size) - { - return new BundleSavedState[size]; - } - }; - - public final Bundle bundle; - - public BundleSavedState(Parcelable superState, Bundle bundle) - { - super(superState); - this.bundle = bundle; - } - - public BundleSavedState(Parcel source, ClassLoader loader) - { - super(source, loader); - this.bundle = source.readBundle(loader); - } - - @Override - public void writeToParcel(Parcel out, int flags) - { - super.writeToParcel(out, flags); - out.writeBundle(bundle); - } -} \ No newline at end of file diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt new file mode 100644 index 000000000..3537259dd --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.ClassLoaderCreator +import androidx.customview.view.AbsSavedState + +class BundleSavedState : AbsSavedState { + val bundle: Bundle? + + constructor(superState: Parcelable?, bundle: Bundle?) : super(superState!!) { + this.bundle = bundle + } + + constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { + bundle = source.readBundle(loader) + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeBundle(bundle) + } + + companion object { + val CREATOR: Parcelable.Creator = + object : ClassLoaderCreator { + override fun createFromParcel( + source: Parcel, + loader: ClassLoader + ): BundleSavedState { + return BundleSavedState(source, loader) + } + + override fun createFromParcel(source: Parcel): BundleSavedState? { + return null + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java deleted file mode 100644 index f9e095b9c..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; - -import androidx.annotation.NonNull; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; - -import java.text.*; -import java.util.*; - -public class FrequencyChart extends ScrollableChart -{ - private Paint pGrid; - - private float em; - - private SimpleDateFormat dfMonth; - - private SimpleDateFormat dfYear; - - private Paint pText, pGraph; - - private RectF rect, prevRect; - - private int baseSize; - - private int paddingTop; - - private float columnWidth; - - private int columnHeight; - - private int nColumns; - - private int textColor; - - private int gridColor; - - private int[] colors; - - private int primaryColor; - - private boolean isBackgroundTransparent; - - @NonNull - private HashMap frequency; - - private int maxFreq; - - private int firstWeekday = Calendar.SUNDAY; - - public FrequencyChart(Context context) - { - super(context); - init(); - } - - public FrequencyChart(Context context, AttributeSet attrs) - { - super(context, attrs); - this.frequency = new HashMap<>(); - init(); - } - - public void setColor(int color) - { - this.primaryColor = color; - initColors(); - postInvalidate(); - } - - public void setFrequency(HashMap frequency) - { - this.frequency = frequency; - maxFreq = getMaxFreq(frequency); - postInvalidate(); - } - - public void setFirstWeekday(int firstWeekday) - { - this.firstWeekday = firstWeekday; - postInvalidate(); - } - - private int getMaxFreq(HashMap frequency) - { - int maxValue = 1; - - for (Integer[] values : frequency.values()) - for (Integer value : values) - maxValue = Math.max(value, maxValue); - - return maxValue; - } - - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) - { - this.isBackgroundTransparent = isBackgroundTransparent; - initColors(); - } - - protected void initPaints() - { - pText = new Paint(); - pText.setAntiAlias(true); - - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); - - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - - rect.set(0, 0, nColumns * columnWidth, columnHeight); - rect.offset(0, paddingTop); - - drawGrid(canvas, rect); - - pText.setTextAlign(Paint.Align.CENTER); - pText.setColor(textColor); - pGraph.setColor(primaryColor); - prevRect.setEmpty(); - - GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendarWithOffset(); - currentDate.set(Calendar.DAY_OF_MONTH, 1); - currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset()); - - for (int i = 0; i < nColumns - 1; i++) - { - rect.set(0, 0, columnWidth, columnHeight); - rect.offset(i * columnWidth, 0); - - drawColumn(canvas, rect, currentDate); - currentDate.add(Calendar.MONTH, 1); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - } - - @Override - protected void onSizeChanged(int width, - int height, - int oldWidth, - int oldHeight) - { - if (height < 9) height = 200; - - baseSize = height / 8; - setScrollerBucketSize(baseSize); - - pText.setTextSize(baseSize * 0.4f); - pGraph.setTextSize(baseSize * 0.4f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(baseSize * 0.05f); - em = pText.getFontSpacing(); - - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - columnHeight = 8 * baseSize; - nColumns = (int) (width / columnWidth); - paddingTop = 0; - } - - private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date) - { - Integer[] values = frequency.get(new Timestamp(date)); - float rowHeight = rect.height() / 8.0f; - prevRect.set(rect); - - Integer[] localeWeekdayList = DateUtils.getWeekdaySequence(firstWeekday); - for (int j = 0; j < localeWeekdayList.length; j++) - { - rect.set(0, 0, baseSize, baseSize); - rect.offset(prevRect.left, prevRect.top + baseSize * j); - - int i = localeWeekdayList[j] % 7; - if (values != null) drawMarker(canvas, rect, values[i]); - - rect.offset(0, rowHeight); - } - - drawFooter(canvas, rect, date); - } - - private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date) - { - Date time = date.getTime(); - - canvas.drawText(dfMonth.format(time), rect.centerX(), - rect.centerY() - 0.1f * em, pText); - - if (date.get(Calendar.MONTH) == 1) - canvas.drawText(dfYear.format(time), rect.centerX(), - rect.centerY() + 0.9f * em, pText); - } - - private void drawGrid(Canvas canvas, RectF rGrid) - { - int nRows = 7; - float rowHeight = rGrid.height() / (nRows + 1); - - pText.setTextAlign(Paint.Align.LEFT); - pText.setColor(textColor); - pGrid.setColor(gridColor); - - for (String day : DateUtils.getShortWeekdayNames(firstWeekday)) - { - canvas.drawText(day, rGrid.right - columnWidth, - rGrid.top + rowHeight / 2 + 0.25f * em, pText); - - pGrid.setStrokeWidth(1f); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, - pGrid); - - rGrid.offset(0, rowHeight); - } - - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); - } - - private void drawMarker(Canvas canvas, RectF rect, Integer value) - { - float padding = rect.height() * 0.2f; - // maximal allowed mark radius - float maxRadius = (rect.height() - 2 * padding) / 2.0f; - // the real mark radius is scaled down by a factor depending on the maximal frequency - float scale = 1.0f/maxFreq * value; - float radius = maxRadius * scale; - - int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale)); - pGraph.setColor(colors[colorIndex]); - canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph); - } - - private float getMaxMonthWidth() - { - float maxMonthWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 12; i++) - { - day.set(Calendar.MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxMonthWidth = Math.max(maxMonthWidth, monthWidth); - } - - return maxMonthWidth; - } - - private void init() - { - initPaints(); - initColors(); - initDateFormats(); - initRects(); - } - - private void initColors() - { - StyledResources res = new StyledResources(getContext()); - textColor = res.getColor(R.attr.mediumContrastTextColor); - gridColor = res.getColor(R.attr.lowContrastTextColor); - - colors = new int[4]; - colors[0] = gridColor; - colors[3] = primaryColor; - colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f); - colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f); - } - - private void initDateFormats() - { - if (isInEditMode()) - { - dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); - dfYear = new SimpleDateFormat("yyyy", Locale.getDefault()); - } - else - { - dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM"); - dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy"); - } - } - - private void initRects() - { - rect = new RectF(); - prevRect = new RectF(); - } - - public void populateWithRandomData() - { - GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); - date.set(Calendar.DAY_OF_MONTH, 1); - Random rand = new Random(); - frequency.clear(); - - for (int i = 0; i < 40; i++) - { - Integer values[] = new Integer[7]; - for (int j = 0; j < 7; j++) - values[j] = rand.nextInt(5); - - frequency.put(new Timestamp(date), values); - date.add(Calendar.MONTH, -1); - } - maxFreq = getMaxFreq(frequency); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt new file mode 100644 index 000000000..f0c85d036 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset +import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence +import org.isoron.uhabits.utils.ColorUtils.mixColors +import org.isoron.uhabits.utils.StyledResources +import org.isoron.uhabits.utils.toSimpleDataFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Locale +import java.util.Random +import kotlin.collections.HashMap + +class FrequencyChart : ScrollableChart { + private var pGrid: Paint? = null + private var em = 0f + private var dfMonth: SimpleDateFormat? = null + private var dfYear: SimpleDateFormat? = null + private var pText: Paint? = null + private var pGraph: Paint? = null + private var rect: RectF? = null + private var prevRect: RectF? = null + private var baseSize = 0 + private var internalPaddingTop = 0 + private var columnWidth = 0f + private var columnHeight = 0 + private var nColumns = 0 + private var textColor = 0 + private var gridColor = 0 + private lateinit var colors: IntArray + private var primaryColor = 0 + private var isBackgroundTransparent = false + private lateinit var frequency: HashMap> + private var maxFreq = 0 + private var firstWeekday = Calendar.SUNDAY + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + frequency = HashMap() + init() + } + + fun setColor(color: Int) { + primaryColor = color + initColors() + postInvalidate() + } + + fun setFrequency(frequency: java.util.HashMap>) { + this.frequency = frequency + maxFreq = getMaxFreq(frequency) + postInvalidate() + } + + fun setFirstWeekday(firstWeekday: Int) { + this.firstWeekday = firstWeekday + postInvalidate() + } + + private fun getMaxFreq(frequency: HashMap>): Int { + var maxValue = 1 + for (values in frequency.values) for (value in values) maxValue = Math.max( + value!!, + maxValue + ) + return maxValue + } + + fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { + this.isBackgroundTransparent = isBackgroundTransparent + initColors() + } + + protected fun initPaints() { + pText = Paint() + pText!!.isAntiAlias = true + pGraph = Paint() + pGraph!!.textAlign = Paint.Align.CENTER + pGraph!!.isAntiAlias = true + pGrid = Paint() + pGrid!!.isAntiAlias = true + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat() + rect!!.offset(0f, internalPaddingTop.toFloat()) + drawGrid(canvas, rect) + pText!!.textAlign = Paint.Align.CENTER + pText!!.color = textColor + pGraph!!.color = primaryColor + prevRect!!.setEmpty() + val currentDate: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + currentDate[Calendar.DAY_OF_MONTH] = 1 + currentDate.add(Calendar.MONTH, -nColumns + 2 - dataOffset) + for (i in 0 until nColumns - 1) { + rect!![0f, 0f, columnWidth] = columnHeight.toFloat() + rect!!.offset(i * columnWidth, 0f) + drawColumn(canvas, rect, currentDate) + currentDate.add(Calendar.MONTH, 1) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + setMeasuredDimension(width, height) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + var height = height + if (height < 9) height = 200 + baseSize = height / 8 + setScrollerBucketSize(baseSize) + pText!!.textSize = baseSize * 0.4f + pGraph!!.textSize = baseSize * 0.4f + pGraph!!.strokeWidth = baseSize * 0.1f + pGrid!!.strokeWidth = baseSize * 0.05f + em = pText!!.fontSpacing + columnWidth = baseSize.toFloat() + columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + columnHeight = 8 * baseSize + nColumns = (width / columnWidth).toInt() + internalPaddingTop = 0 + } + + private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { + val values = frequency[Timestamp(date)] + val rowHeight = rect!!.height() / 8.0f + prevRect!!.set(rect) + val localeWeekdayList: Array = getWeekdaySequence(firstWeekday) + for (j in localeWeekdayList.indices) { + rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() + rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) + val i = localeWeekdayList[j] % 7 + if (values != null) drawMarker(canvas, rect, values[i]) + rect.offset(0f, rowHeight) + } + drawFooter(canvas, rect, date) + } + + private fun drawFooter(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { + val time = date.time + canvas.drawText( + dfMonth!!.format(time), + rect!!.centerX(), + rect.centerY() - 0.1f * em, + pText!! + ) + if (date[Calendar.MONTH] == 1) canvas.drawText( + dfYear!!.format(time), + rect.centerX(), + rect.centerY() + 0.9f * em, + pText!! + ) + } + + private fun drawGrid(canvas: Canvas, rGrid: RectF?) { + val nRows = 7 + val rowHeight = rGrid!!.height() / (nRows + 1) + pText!!.textAlign = Paint.Align.LEFT + pText!!.color = textColor + pGrid!!.color = gridColor + for (day in getShortWeekdayNames(firstWeekday)) { + canvas.drawText( + day, + rGrid.right - columnWidth, + rGrid.top + rowHeight / 2 + 0.25f * em, + pText!! + ) + pGrid!!.strokeWidth = 1f + canvas.drawLine( + rGrid.left, + rGrid.top, + rGrid.right, + rGrid.top, + pGrid!! + ) + rGrid.offset(0f, rowHeight) + } + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) + } + + private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) { + val padding = rect!!.height() * 0.2f + // maximal allowed mark radius + val maxRadius = (rect.height() - 2 * padding) / 2.0f + // the real mark radius is scaled down by a factor depending on the maximal frequency + val scale = 1.0f / maxFreq * value!! + val radius = maxRadius * scale + val colorIndex = Math.min(colors.size - 1, Math.round((colors.size - 1) * scale)) + pGraph!!.color = colors[colorIndex] + canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph!!) + } + + private val maxMonthWidth: Float + private get() { + var maxMonthWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..11) { + day[Calendar.MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + } + return maxMonthWidth + } + + private fun init() { + initPaints() + initColors() + initDateFormats() + initRects() + } + + private fun initColors() { + val res = StyledResources(context) + textColor = res.getColor(R.attr.mediumContrastTextColor) + gridColor = res.getColor(R.attr.lowContrastTextColor) + colors = IntArray(4) + colors[0] = gridColor + colors[3] = primaryColor + colors[1] = mixColors(colors[0], colors[3], 0.66f) + colors[2] = mixColors(colors[0], colors[3], 0.33f) + } + + private fun initDateFormats() { + if (isInEditMode) { + dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) + dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) + } else { + dfMonth = "MMM".toSimpleDataFormat() + dfYear = "yyyy".toSimpleDataFormat() + } + } + + private fun initRects() { + rect = RectF() + prevRect = RectF() + } + + fun populateWithRandomData() { + val date: GregorianCalendar = getStartOfTodayCalendar() + date[Calendar.DAY_OF_MONTH] = 1 + val rand = Random() + frequency.clear() + for (i in 0..39) { + val values = IntArray(7) { rand.nextInt(5) }.toTypedArray() + frequency[Timestamp(date)] = values + date.add(Calendar.MONTH, -1) + } + maxFreq = getMaxFreq(frequency) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt similarity index 80% rename from uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java rename to uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt index a973da9f0..a7882c80f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt @@ -16,14 +16,11 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.activities.common.views -package org.isoron.uhabits.activities.common.views; +import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Habit; - -public interface HabitChart -{ - void setHabit(Habit habit); - - void refreshData(); +interface HabitChart { + fun setHabit(habit: Habit?) + fun refreshData() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java deleted file mode 100644 index df3b6ff80..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.text.*; -import android.util.*; -import android.view.*; - -import androidx.annotation.Nullable; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.utils.*; - -import static org.isoron.uhabits.utils.AttributeSetUtils.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class RingView extends View -{ - public static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - - private int color; - - private float precision; - - private float percentage; - - private int diameter; - - private float thickness; - - private RectF rect; - - private TextPaint pRing; - - private Integer backgroundColor; - - private Integer inactiveColor; - - private float em; - - private String text; - - private float textSize; - - private boolean enableFontAwesome; - - @Nullable - private Bitmap drawingCache; - - private Canvas cacheCanvas; - - private boolean isTransparencyEnabled; - - public RingView(Context context) - { - super(context); - - percentage = 0.0f; - precision = 0.01f; - color = PaletteUtils.getAndroidTestColor(0); - thickness = dpToPixels(getContext(), 2); - text = ""; - textSize = getDimension(context, R.dimen.smallTextSize); - - init(); - } - - public RingView(Context ctx, AttributeSet attrs) - { - super(ctx, attrs); - - percentage = getFloatAttribute(ctx, attrs, "percentage", 0); - precision = getFloatAttribute(ctx, attrs, "precision", 0.01f); - - color = getColorAttribute(ctx, attrs, "color", 0); - backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null); - inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null); - - thickness = getFloatAttribute(ctx, attrs, "thickness", 0); - thickness = dpToPixels(ctx, thickness); - - float defaultTextSize = getDimension(ctx, R.dimen.smallTextSize); - textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize); - textSize = spToPixels(ctx, textSize); - text = getAttribute(ctx, attrs, "text", ""); - - enableFontAwesome = - getBooleanAttribute(ctx, attrs, "enableFontAwesome", false); - - init(); - } - - @Override - public void setBackgroundColor(int backgroundColor) - { - this.backgroundColor = backgroundColor; - invalidate(); - } - - public void setColor(int color) - { - this.color = color; - invalidate(); - } - - public int getColor() - { - return color; - } - - public void setIsTransparencyEnabled(boolean isTransparencyEnabled) - { - this.isTransparencyEnabled = isTransparencyEnabled; - } - - public void setPercentage(float percentage) - { - this.percentage = percentage; - invalidate(); - } - - public void setPrecision(float precision) - { - this.precision = precision; - invalidate(); - } - - public void setText(String text) - { - this.text = text; - invalidate(); - } - - public void setTextSize(float textSize) - { - this.textSize = textSize; - } - - public void setThickness(float thickness) - { - this.thickness = thickness; - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - Canvas activeCanvas; - - if (isTransparencyEnabled) - { - if (drawingCache == null) reallocateCache(); - activeCanvas = cacheCanvas; - drawingCache.eraseColor(Color.TRANSPARENT); - } - else - { - activeCanvas = canvas; - } - - pRing.setColor(color); - rect.set(0, 0, diameter, diameter); - - float angle = 360 * Math.round(percentage / precision) * precision; - - activeCanvas.drawArc(rect, -90, angle, true, pRing); - - pRing.setColor(inactiveColor); - activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing); - - if (thickness > 0) - { - if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR); - else pRing.setColor(backgroundColor); - - rect.inset(thickness, thickness); - activeCanvas.drawArc(rect, 0, 360, true, pRing); - pRing.setXfermode(null); - - pRing.setColor(color); - pRing.setTextSize(textSize); - if (enableFontAwesome) - pRing.setTypeface(getFontAwesome(getContext())); - activeCanvas.drawText(text, rect.centerX(), - rect.centerY() + 0.4f * em, pRing); - } - - if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - diameter = Math.min(height, width); - - pRing.setTextSize(textSize); - em = pRing.measureText("M"); - - setMeasuredDimension(diameter, diameter); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) - { - super.onSizeChanged(w, h, oldw, oldh); - - if (isTransparencyEnabled) reallocateCache(); - } - - private void init() - { - pRing = new TextPaint(); - pRing.setAntiAlias(true); - pRing.setColor(color); - pRing.setTextAlign(Paint.Align.CENTER); - - StyledResources res = new StyledResources(getContext()); - - if (backgroundColor == null) - backgroundColor = res.getColor(R.attr.cardBgColor); - - if (inactiveColor == null) - inactiveColor = res.getColor(R.attr.highContrastTextColor); - - inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f); - - rect = new RectF(); - } - - private void reallocateCache() - { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = - Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); - } - - public float getPercentage() - { - return percentage; - } - - public float getPrecision() - { - return precision; - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt new file mode 100644 index 000000000..67635bfaf --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import org.isoron.uhabits.R +import org.isoron.uhabits.utils.AttributeSetUtils.getAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getBooleanAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getColorAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getFloatAttribute +import org.isoron.uhabits.utils.ColorUtils.setAlpha +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome +import org.isoron.uhabits.utils.InterfaceUtils.spToPixels +import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor +import org.isoron.uhabits.utils.StyledResources + +class RingView : View { + private var color: Int + private var precision: Float + private var percentage: Float + private var diameter = 0 + private var thickness: Float + private var rect: RectF? = null + private var pRing: TextPaint? = null + private var backgroundColor: Int? = null + private var inactiveColor: Int? = null + private var em = 0f + private var text: String? + private var textSize: Float + private var enableFontAwesome = false + private var internalDrawingCache: Bitmap? = null + private var cacheCanvas: Canvas? = null + private var isTransparencyEnabled = false + + constructor(context: Context?) : super(context) { + percentage = 0.0f + precision = 0.01f + color = getAndroidTestColor(0) + thickness = dpToPixels(getContext(), 2f) + text = "" + textSize = getDimension(context!!, R.dimen.smallTextSize) + init() + } + + constructor(ctx: Context?, attrs: AttributeSet?) : super(ctx, attrs) { + percentage = getFloatAttribute(ctx!!, attrs!!, "percentage", 0f) + precision = getFloatAttribute(ctx, attrs, "precision", 0.01f) + color = getColorAttribute(ctx, attrs, "color", 0)!! + backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null) + inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null) + thickness = getFloatAttribute(ctx, attrs, "thickness", 0f) + thickness = dpToPixels(ctx, thickness) + val defaultTextSize = getDimension(ctx, R.dimen.smallTextSize) + textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize) + textSize = spToPixels(ctx, textSize) + text = getAttribute(ctx, attrs, "text", "") + enableFontAwesome = getBooleanAttribute(ctx, attrs, "enableFontAwesome", false) + init() + } + + override fun setBackgroundColor(backgroundColor: Int) { + this.backgroundColor = backgroundColor + invalidate() + } + + fun setColor(color: Int) { + this.color = color + invalidate() + } + + fun getColor(): Int { + return color + } + + fun setIsTransparencyEnabled(isTransparencyEnabled: Boolean) { + this.isTransparencyEnabled = isTransparencyEnabled + } + + fun setPercentage(percentage: Float) { + this.percentage = percentage + invalidate() + } + + fun setPrecision(precision: Float) { + this.precision = precision + invalidate() + } + + fun setText(text: String?) { + this.text = text + invalidate() + } + + fun setTextSize(textSize: Float) { + this.textSize = textSize + } + + fun setThickness(thickness: Float) { + this.thickness = thickness + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val activeCanvas: Canvas? + if (isTransparencyEnabled) { + if (internalDrawingCache == null) reallocateCache() + activeCanvas = cacheCanvas + internalDrawingCache!!.eraseColor(Color.TRANSPARENT) + } else { + activeCanvas = canvas + } + pRing!!.color = color + rect!![0f, 0f, diameter.toFloat()] = diameter.toFloat() + val angle = 360 * Math.round(percentage / precision) * precision + activeCanvas!!.drawArc(rect!!, -90f, angle, true, pRing!!) + pRing!!.color = inactiveColor!! + activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!) + if (thickness > 0) { + if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color = + backgroundColor!! + rect!!.inset(thickness, thickness) + activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!) + pRing!!.xfermode = null + pRing!!.color = color + pRing!!.textSize = textSize + if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context) + activeCanvas.drawText( + text!!, + rect!!.centerX(), + rect!!.centerY() + 0.4f * em, + pRing!! + ) + } + if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + diameter = Math.min(height, width) + pRing!!.textSize = textSize + em = pRing!!.measureText("M") + setMeasuredDimension(diameter, diameter) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (isTransparencyEnabled) reallocateCache() + } + + private fun init() { + pRing = TextPaint() + pRing!!.isAntiAlias = true + pRing!!.color = color + pRing!!.textAlign = Paint.Align.CENTER + val res = StyledResources(context) + if (backgroundColor == null) backgroundColor = res.getColor(R.attr.cardBgColor) + if (inactiveColor == null) inactiveColor = res.getColor(R.attr.highContrastTextColor) + inactiveColor = setAlpha(inactiveColor!!, 0.1f) + rect = RectF() + } + + private fun reallocateCache() { + if (internalDrawingCache != null) internalDrawingCache!!.recycle() + val newDrawingCache = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888) + internalDrawingCache = newDrawingCache + cacheCanvas = Canvas(newDrawingCache) + } + + fun getPercentage(): Float { + return percentage + } + + fun getPrecision(): Float { + return precision + } + + companion object { + val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java deleted file mode 100644 index 60b47c7a3..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; - -import java.text.*; -import java.util.*; - -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class ScoreChart extends ScrollableChart -{ - private static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - - private static final PorterDuffXfermode XFERMODE_SRC = - new PorterDuffXfermode(PorterDuff.Mode.SRC); - - private Paint pGrid; - - private float em; - - private SimpleDateFormat dfMonth; - - private SimpleDateFormat dfDay; - - private SimpleDateFormat dfYear; - - private Paint pText, pGraph; - - private RectF rect, prevRect; - - private int baseSize; - - private int paddingTop; - - private float columnWidth; - - private int columnHeight; - - private int nColumns; - - private int textColor; - - private int gridColor; - - @Nullable - private List scores; - - private int primaryColor; - - @Deprecated - private int bucketSize = 7; - - private int backgroundColor; - - private Bitmap drawingCache; - - private Canvas cacheCanvas; - - private boolean isTransparencyEnabled; - - private int skipYear = 0; - - private String previousYearText; - - private String previousMonthText; - - public ScoreChart(Context context) - { - super(context); - init(); - } - - public ScoreChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - public void populateWithRandomData() - { - Random random = new Random(); - scores = new LinkedList<>(); - - double previous = 0.5f; - Timestamp timestamp = DateUtils.getToday(); - - for (int i = 1; i < 100; i++) - { - double step = 0.1f; - double current = previous + random.nextDouble() * step * 2 - step; - current = Math.max(0, Math.min(1.0f, current)); - scores.add(new Score(timestamp.minus(i), current)); - previous = current; - } - } - - public void setBucketSize(int bucketSize) - { - this.bucketSize = bucketSize; - postInvalidate(); - } - - public void setIsTransparencyEnabled(boolean enabled) - { - this.isTransparencyEnabled = enabled; - postInvalidate(); - } - - public void setColor(int primaryColor) - { - this.primaryColor = primaryColor; - postInvalidate(); - } - - public void setScores(@NonNull List scores) - { - this.scores = scores; - postInvalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - Canvas activeCanvas; - - if (isTransparencyEnabled) - { - if (drawingCache == null) initCache(getWidth(), getHeight()); - - activeCanvas = cacheCanvas; - drawingCache.eraseColor(Color.TRANSPARENT); - } - else - { - activeCanvas = canvas; - } - - if (scores == null) return; - - rect.set(0, 0, nColumns * columnWidth, columnHeight); - rect.offset(0, paddingTop); - - drawGrid(activeCanvas, rect); - - pText.setColor(textColor); - pGraph.setColor(primaryColor); - prevRect.setEmpty(); - - previousMonthText = ""; - previousYearText = ""; - skipYear = 0; - - for (int k = 0; k < nColumns; k++) - { - int offset = nColumns - k - 1 + getDataOffset(); - if (offset >= scores.size()) continue; - - double score = scores.get(offset).getValue(); - Timestamp timestamp = scores.get(offset).getTimestamp(); - - int height = (int) (columnHeight * score); - - rect.set(0, 0, baseSize, baseSize); - rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, - paddingTop + columnHeight - height - baseSize / 2); - - if (!prevRect.isEmpty()) - { - drawLine(activeCanvas, prevRect, rect); - drawMarker(activeCanvas, prevRect); - } - - if (k == nColumns - 1) drawMarker(activeCanvas, rect); - - prevRect.set(rect); - rect.set(0, 0, columnWidth, columnHeight); - rect.offset(k * columnWidth, paddingTop); - - drawFooter(activeCanvas, rect, timestamp); - } - - if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - } - - @Override - protected void onSizeChanged(int width, - int height, - int oldWidth, - int oldHeight) - { - if (height < 9) height = 200; - - float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize); - float textSize = height * 0.06f; - pText.setTextSize(Math.min(textSize, maxTextSize)); - em = pText.getFontSpacing(); - - int footerHeight = (int) (3 * em); - paddingTop = (int) (em); - - baseSize = (height - footerHeight - paddingTop) / 8; - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - nColumns = (int) (width / columnWidth); - columnWidth = (float) width / nColumns; - setScrollerBucketSize((int) columnWidth); - - columnHeight = 8 * baseSize; - - float minStrokeWidth = dpToPixels(getContext(), 1); - pGraph.setTextSize(baseSize * 0.5f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); - - if (isTransparencyEnabled) initCache(width, height); - } - - private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate) - { - String yearText = dfYear.format(currentDate.toJavaDate()); - String monthText = dfMonth.format(currentDate.toJavaDate()); - String dayText = dfDay.format(currentDate.toJavaDate()); - - GregorianCalendar calendar = currentDate.toCalendar(); - - String text; - int year = calendar.get(Calendar.YEAR); - - boolean shouldPrintYear = true; - if (yearText.equals(previousYearText)) shouldPrintYear = false; - if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; - - if (skipYear > 0) - { - skipYear--; - shouldPrintYear = false; - } - - if (shouldPrintYear) - { - previousYearText = yearText; - previousMonthText = ""; - - pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, - pText); - - skipYear = 1; - } - - if (bucketSize < 365) - { - if (!monthText.equals(previousMonthText)) - { - previousMonthText = monthText; - text = monthText; - } - else - { - text = dayText; - } - - pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, - pText); - } - } - - private void drawGrid(Canvas canvas, RectF rGrid) - { - int nRows = 5; - float rowHeight = rGrid.height() / nRows; - - pText.setTextAlign(Paint.Align.LEFT); - pText.setColor(textColor); - pGrid.setColor(gridColor); - - for (int i = 0; i < nRows; i++) - { - canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), - rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, - pGrid); - rGrid.offset(0, rowHeight); - } - - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); - } - - private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo) - { - pGraph.setColor(primaryColor); - canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), - rectTo.centerX(), rectTo.centerY(), pGraph); - } - - private void drawMarker(Canvas canvas, RectF rect) - { - rect.inset(baseSize * 0.225f, baseSize * 0.225f); - setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); - canvas.drawOval(rect, pGraph); - - rect.inset(baseSize * 0.1f, baseSize * 0.1f); - setModeOrColor(pGraph, XFERMODE_SRC, primaryColor); - canvas.drawOval(rect, pGraph); - -// rect.inset(baseSize * 0.1f, baseSize * 0.1f); -// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); -// canvas.drawOval(rect, pGraph); - - if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC); - } - - private float getMaxDayWidth() - { - float maxDayWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 28; i++) - { - day.set(Calendar.DAY_OF_MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxDayWidth = Math.max(maxDayWidth, monthWidth); - } - - return maxDayWidth; - } - - private float getMaxMonthWidth() - { - float maxMonthWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 12; i++) - { - day.set(Calendar.MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxMonthWidth = Math.max(maxMonthWidth, monthWidth); - } - - return maxMonthWidth; - } - - private void init() - { - initPaints(); - initColors(); - initDateFormats(); - initRects(); - } - - private void initCache(int width, int height) - { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = - Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); - } - - private void initColors() - { - StyledResources res = new StyledResources(getContext()); - - primaryColor = Color.BLACK; - textColor = res.getColor(R.attr.mediumContrastTextColor); - gridColor = res.getColor(R.attr.lowContrastTextColor); - backgroundColor = res.getColor(R.attr.cardBgColor); - } - - private void initDateFormats() - { - if (isInEditMode()) - { - dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); - dfYear = new SimpleDateFormat("yyyy", Locale.getDefault()); - dfDay = new SimpleDateFormat("d", Locale.getDefault()); - - } - else - { - dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM"); - dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy"); - dfDay = DateExtensionsKt.toSimpleDataFormat("d"); - } - } - - private void initPaints() - { - pText = new Paint(); - pText.setAntiAlias(true); - - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); - - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - - private void initRects() - { - rect = new RectF(); - prevRect = new RectF(); - } - - private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) - { - if (isTransparencyEnabled) p.setXfermode(mode); - else p.setColor(color); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt new file mode 100644 index 000000000..9a0a5bb76 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.util.AttributeSet +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Score +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources +import org.isoron.uhabits.utils.toSimpleDataFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.LinkedList +import java.util.Locale +import java.util.Random + +class ScoreChart : ScrollableChart { + private var pGrid: Paint? = null + private var em = 0f + private var dfMonth: SimpleDateFormat? = null + private var dfDay: SimpleDateFormat? = null + private var dfYear: SimpleDateFormat? = null + private var pText: Paint? = null + private var pGraph: Paint? = null + private var rect: RectF? = null + private var prevRect: RectF? = null + private var baseSize = 0 + private var internalPaddingTop = 0 + private var columnWidth = 0f + private var columnHeight = 0 + private var nColumns = 0 + private var textColor = 0 + private var gridColor = 0 + private var scores: MutableList? = null + private var primaryColor = 0 + + @Deprecated("") + private var bucketSize = 7 + private var internalBackgroundColor = 0 + private var internalDrawingCache: Bitmap? = null + private var cacheCanvas: Canvas? = null + private var isTransparencyEnabled = false + private var skipYear = 0 + private var previousYearText: String? = null + private var previousMonthText: String? = null + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + fun populateWithRandomData() { + val random = Random() + val newScores = LinkedList() + var previous = 0.5 + val timestamp: Timestamp = getToday() + for (i in 1..99) { + val step = 0.1 + var current = previous + random.nextDouble() * step * 2 - step + current = Math.max(0.0, Math.min(1.0, current)) + newScores.add(Score(timestamp.minus(i), current)) + previous = current + } + scores = newScores + } + + fun setBucketSize(bucketSize: Int) { + this.bucketSize = bucketSize + postInvalidate() + } + + fun setIsTransparencyEnabled(enabled: Boolean) { + isTransparencyEnabled = enabled + postInvalidate() + } + + fun setColor(primaryColor: Int) { + this.primaryColor = primaryColor + postInvalidate() + } + + fun setScores(scores: MutableList) { + this.scores = scores + postInvalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val activeCanvas: Canvas? + if (isTransparencyEnabled) { + if (internalDrawingCache == null) initCache(width, height) + activeCanvas = cacheCanvas + internalDrawingCache!!.eraseColor(Color.TRANSPARENT) + } else { + activeCanvas = canvas + } + if (scores == null) return + rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat() + rect!!.offset(0f, internalPaddingTop.toFloat()) + drawGrid(activeCanvas, rect) + pText!!.color = textColor + pGraph!!.color = primaryColor + prevRect!!.setEmpty() + previousMonthText = "" + previousYearText = "" + skipYear = 0 + for (k in 0 until nColumns) { + val offset = nColumns - k - 1 + dataOffset + if (offset >= scores!!.size) continue + val score = scores!![offset].value + val timestamp = scores!![offset].timestamp + val height = (columnHeight * score).toInt() + rect!![0f, 0f, baseSize.toFloat()] = baseSize.toFloat() + rect!!.offset( + k * columnWidth + (columnWidth - baseSize) / 2, + ( + internalPaddingTop + columnHeight - height - baseSize / 2 + ).toFloat() + ) + if (!prevRect!!.isEmpty) { + drawLine(activeCanvas, prevRect, rect) + drawMarker(activeCanvas, prevRect) + } + if (k == nColumns - 1) drawMarker(activeCanvas, rect) + prevRect!!.set(rect) + rect!![0f, 0f, columnWidth] = columnHeight.toFloat() + rect!!.offset(k * columnWidth, internalPaddingTop.toFloat()) + drawFooter(activeCanvas, rect, timestamp) + } + if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + setMeasuredDimension(width, height) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + var height = height + if (height < 9) height = 200 + val maxTextSize = getDimension(context, R.dimen.tinyTextSize) + val textSize = height * 0.06f + pText!!.textSize = Math.min(textSize, maxTextSize) + em = pText!!.fontSpacing + val footerHeight = (3 * em).toInt() + internalPaddingTop = em.toInt() + baseSize = (height - footerHeight - internalPaddingTop) / 8 + columnWidth = baseSize.toFloat() + columnWidth = Math.max(columnWidth, maxDayWidth * 1.5f) + columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + nColumns = (width / columnWidth).toInt() + columnWidth = width.toFloat() / nColumns + setScrollerBucketSize(columnWidth.toInt()) + columnHeight = 8 * baseSize + val minStrokeWidth = dpToPixels(context, 1f) + pGraph!!.textSize = baseSize * 0.5f + pGraph!!.strokeWidth = baseSize * 0.1f + pGrid!!.strokeWidth = Math.min(minStrokeWidth, baseSize * 0.05f) + if (isTransparencyEnabled) initCache(width, height) + } + + private fun drawFooter(canvas: Canvas?, rect: RectF?, currentDate: Timestamp) { + val yearText = dfYear!!.format(currentDate.toJavaDate()) + val monthText = dfMonth!!.format(currentDate.toJavaDate()) + val dayText = dfDay!!.format(currentDate.toJavaDate()) + val calendar = currentDate.toCalendar() + val text: String + val year = calendar[Calendar.YEAR] + var shouldPrintYear = true + if (yearText == previousYearText) shouldPrintYear = false + if (bucketSize >= 365 && year % 2 != 0) shouldPrintYear = false + if (skipYear > 0) { + skipYear-- + shouldPrintYear = false + } + if (shouldPrintYear) { + previousYearText = yearText + previousMonthText = "" + pText!!.textAlign = Paint.Align.CENTER + canvas!!.drawText( + yearText, + rect!!.centerX(), + rect.bottom + em * 2.2f, + pText!! + ) + skipYear = 1 + } + if (bucketSize < 365) { + if (monthText != previousMonthText) { + previousMonthText = monthText + text = monthText + } else { + text = dayText + } + pText!!.textAlign = Paint.Align.CENTER + canvas!!.drawText( + text, + rect!!.centerX(), + rect.bottom + em * 1.2f, + pText!! + ) + } + } + + private fun drawGrid(canvas: Canvas?, rGrid: RectF?) { + val nRows = 5 + val rowHeight = rGrid!!.height() / nRows + pText!!.textAlign = Paint.Align.LEFT + pText!!.color = textColor + pGrid!!.color = gridColor + for (i in 0 until nRows) { + canvas!!.drawText( + String.format("%d%%", 100 - i * 100 / nRows), + rGrid.left + 0.5f * em, + rGrid.top + 1f * em, + pText!! + ) + canvas.drawLine( + rGrid.left, + rGrid.top, + rGrid.right, + rGrid.top, + pGrid!! + ) + rGrid.offset(0f, rowHeight) + } + canvas!!.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) + } + + private fun drawLine(canvas: Canvas?, rectFrom: RectF?, rectTo: RectF?) { + pGraph!!.color = primaryColor + canvas!!.drawLine( + rectFrom!!.centerX(), + rectFrom.centerY(), + rectTo!!.centerX(), + rectTo.centerY(), + pGraph!! + ) + } + + private fun drawMarker(canvas: Canvas?, rect: RectF?) { + rect!!.inset(baseSize * 0.225f, baseSize * 0.225f) + setModeOrColor(pGraph, XFERMODE_CLEAR, internalBackgroundColor) + canvas!!.drawOval(rect, pGraph!!) + rect.inset(baseSize * 0.1f, baseSize * 0.1f) + setModeOrColor(pGraph, XFERMODE_SRC, primaryColor) + canvas.drawOval(rect, pGraph!!) + +// rect.inset(baseSize * 0.1f, baseSize * 0.1f); +// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); +// canvas.drawOval(rect, pGraph); + if (isTransparencyEnabled) pGraph!!.xfermode = XFERMODE_SRC + } + + private val maxDayWidth: Float + private get() { + var maxDayWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..27) { + day[Calendar.DAY_OF_MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxDayWidth = Math.max(maxDayWidth, monthWidth) + } + return maxDayWidth + } + private val maxMonthWidth: Float + private get() { + var maxMonthWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..11) { + day[Calendar.MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + } + return maxMonthWidth + } + + private fun init() { + initPaints() + initColors() + initDateFormats() + initRects() + } + + private fun initCache(width: Int, height: Int) { + if (internalDrawingCache != null) internalDrawingCache!!.recycle() + val newDrawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + internalDrawingCache = newDrawingCache + cacheCanvas = Canvas(newDrawingCache) + } + + private fun initColors() { + val res = StyledResources(context) + primaryColor = Color.BLACK + textColor = res.getColor(R.attr.mediumContrastTextColor) + gridColor = res.getColor(R.attr.lowContrastTextColor) + internalBackgroundColor = res.getColor(R.attr.cardBgColor) + } + + private fun initDateFormats() { + if (isInEditMode) { + dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) + dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) + dfDay = SimpleDateFormat("d", Locale.getDefault()) + } else { + dfMonth = "MMM".toSimpleDataFormat() + dfYear = "yyyy".toSimpleDataFormat() + dfDay = "d".toSimpleDataFormat() + } + } + + private fun initPaints() { + pText = Paint() + pText!!.isAntiAlias = true + pGraph = Paint() + pGraph!!.textAlign = Paint.Align.CENTER + pGraph!!.isAntiAlias = true + pGrid = Paint() + pGrid!!.isAntiAlias = true + } + + private fun initRects() { + rect = RectF() + prevRect = RectF() + } + + private fun setModeOrColor(p: Paint?, mode: PorterDuffXfermode, color: Int) { + if (isTransparencyEnabled) p!!.xfermode = mode else p!!.color = color + } + + companion object { + private val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + private val XFERMODE_SRC = PorterDuffXfermode(PorterDuff.Mode.SRC) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java deleted file mode 100644 index 5de149283..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.animation.*; -import android.content.*; -import android.os.*; -import android.util.*; -import android.view.*; -import android.widget.*; - -public abstract class ScrollableChart extends View - implements GestureDetector.OnGestureListener, - ValueAnimator.AnimatorUpdateListener -{ - - private int dataOffset; - - private int scrollerBucketSize = 1; - - private int direction = 1; - - private GestureDetector detector; - - private Scroller scroller; - - private ValueAnimator scrollAnimator; - - private ScrollController scrollController; - - private int maxDataOffset = 10000; - - public ScrollableChart(Context context) - { - super(context); - init(context); - } - - public ScrollableChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(context); - } - - public int getDataOffset() - { - return dataOffset; - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) - { - if (!scroller.isFinished()) - { - scroller.computeScrollOffset(); - updateDataOffset(); - } - else - { - scrollAnimator.cancel(); - } - } - - @Override - public boolean onDown(MotionEvent e) - { - return true; - } - - @Override - public boolean onFling(MotionEvent e1, - MotionEvent e2, - float velocityX, - float velocityY) - { - scroller.fling(scroller.getCurrX(), scroller.getCurrY(), - direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0); - invalidate(); - - scrollAnimator.setDuration(scroller.getDuration()); - scrollAnimator.start(); - return false; - } - - private int getMaxX() - { - return maxDataOffset * scrollerBucketSize; - } - - @Override - public void onRestoreInstanceState(Parcelable state) - { - if(!(state instanceof BundleSavedState)) - { - super.onRestoreInstanceState(state); - return; - } - - BundleSavedState bss = (BundleSavedState) state; - int x = bss.bundle.getInt("x"); - int y = bss.bundle.getInt("y"); - direction = bss.bundle.getInt("direction"); - dataOffset = bss.bundle.getInt("dataOffset"); - maxDataOffset = bss.bundle.getInt("maxDataOffset"); - scroller.startScroll(0, 0, x, y, 0); - scroller.computeScrollOffset(); - super.onRestoreInstanceState(bss.getSuperState()); - } - - @Override - public Parcelable onSaveInstanceState() - { - Parcelable superState = super.onSaveInstanceState(); - Bundle bundle = new Bundle(); - bundle.putInt("x", scroller.getCurrX()); - bundle.putInt("y", scroller.getCurrY()); - bundle.putInt("dataOffset", dataOffset); - bundle.putInt("direction", direction); - bundle.putInt("maxDataOffset", maxDataOffset); - return new BundleSavedState(superState, bundle); - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) - { - if (scrollerBucketSize == 0) return false; - - if (Math.abs(dx) > Math.abs(dy)) - { - ViewParent parent = getParent(); - if (parent != null) parent.requestDisallowInterceptTouchEvent(true); - } - - - dx = - direction * dx; - dx = Math.min(dx, getMaxX() - scroller.getCurrX()); - scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx, - (int) dy, 0); - - scroller.computeScrollOffset(); - updateDataOffset(); - return true; - } - - @Override - public void onShowPress(MotionEvent e) - { - - } - - @Override - public boolean onSingleTapUp(MotionEvent e) - { - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) - { - return detector.onTouchEvent(event); - } - - public void setScrollDirection(int direction) - { - if (direction != 1 && direction != -1) - throw new IllegalArgumentException(); - this.direction = direction; - } - - @Override - public void onLongPress(MotionEvent e) - { - - } - - public void setMaxDataOffset(int maxDataOffset) - { - this.maxDataOffset = maxDataOffset; - this.dataOffset = Math.min(dataOffset, maxDataOffset); - scrollController.onDataOffsetChanged(this.dataOffset); - postInvalidate(); - } - - public void setScrollController(ScrollController scrollController) - { - this.scrollController = scrollController; - } - - public void setScrollerBucketSize(int scrollerBucketSize) - { - this.scrollerBucketSize = scrollerBucketSize; - } - - private void init(Context context) - { - detector = new GestureDetector(context, this); - scroller = new Scroller(context, null, true); - scrollAnimator = ValueAnimator.ofFloat(0, 1); - scrollAnimator.addUpdateListener(this); - scrollController = new ScrollController() {}; - } - - public void reset() - { - scroller.setFinalX(0); - scroller.computeScrollOffset(); - updateDataOffset(); - } - - private void updateDataOffset() - { - int newDataOffset = scroller.getCurrX() / scrollerBucketSize; - newDataOffset = Math.max(0, newDataOffset); - newDataOffset = Math.min(maxDataOffset, newDataOffset); - - if (newDataOffset != dataOffset) - { - dataOffset = newDataOffset; - scrollController.onDataOffsetChanged(dataOffset); - postInvalidate(); - } - } - - public interface ScrollController - { - default void onDataOffsetChanged(int newDataOffset) {} - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt new file mode 100644 index 000000000..a1c86449a --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.widget.Scroller + +abstract class ScrollableChart : View, GestureDetector.OnGestureListener, AnimatorUpdateListener { + var dataOffset = 0 + private set + private var scrollerBucketSize = 1 + private var direction = 1 + private var detector: GestureDetector? = null + private var scroller: Scroller? = null + private var scrollAnimator: ValueAnimator? = null + private var scrollController: ScrollController? = null + private var maxDataOffset = 10000 + + constructor(context: Context?) : super(context) { + init(context) + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init(context) + } + + override fun onAnimationUpdate(animation: ValueAnimator) { + if (!scroller!!.isFinished) { + scroller!!.computeScrollOffset() + updateDataOffset() + } else { + scrollAnimator!!.cancel() + } + } + + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + scroller!!.fling( + scroller!!.currX, + scroller!!.currY, + direction * velocityX.toInt() / 2, + 0, + 0, + maxX, + 0, + 0 + ) + invalidate() + scrollAnimator!!.duration = scroller!!.duration.toLong() + scrollAnimator!!.start() + return false + } + + private val maxX: Int + private get() = maxDataOffset * scrollerBucketSize + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is BundleSavedState) { + super.onRestoreInstanceState(state) + return + } + val bss = state + val x = bss.bundle!!.getInt("x") + val y = bss.bundle.getInt("y") + direction = bss.bundle.getInt("direction") + dataOffset = bss.bundle.getInt("dataOffset") + maxDataOffset = bss.bundle.getInt("maxDataOffset") + scroller!!.startScroll(0, 0, x, y, 0) + scroller!!.computeScrollOffset() + super.onRestoreInstanceState(bss.superState) + } + + public override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + val bundle = Bundle() + bundle.putInt("x", scroller!!.currX) + bundle.putInt("y", scroller!!.currY) + bundle.putInt("dataOffset", dataOffset) + bundle.putInt("direction", direction) + bundle.putInt("maxDataOffset", maxDataOffset) + return BundleSavedState(superState, bundle) + } + + override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean { + var dx = dx + if (scrollerBucketSize == 0) return false + if (Math.abs(dx) > Math.abs(dy)) { + val parent = parent + parent?.requestDisallowInterceptTouchEvent(true) + } + dx = -direction * dx + dx = Math.min(dx, (maxX - scroller!!.currX).toFloat()) + scroller!!.startScroll( + scroller!!.currX, + scroller!!.currY, + dx.toInt(), + dy.toInt(), + 0 + ) + scroller!!.computeScrollOffset() + updateDataOffset() + return true + } + + override fun onShowPress(e: MotionEvent) {} + override fun onSingleTapUp(e: MotionEvent): Boolean { + return false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + return detector!!.onTouchEvent(event) + } + + fun setScrollDirection(direction: Int) { + require(!(direction != 1 && direction != -1)) + this.direction = direction + } + + override fun onLongPress(e: MotionEvent) {} + fun setMaxDataOffset(maxDataOffset: Int) { + this.maxDataOffset = maxDataOffset + dataOffset = Math.min(dataOffset, maxDataOffset) + scrollController!!.onDataOffsetChanged(dataOffset) + postInvalidate() + } + + fun setScrollController(scrollController: ScrollController?) { + this.scrollController = scrollController + } + + fun setScrollerBucketSize(scrollerBucketSize: Int) { + this.scrollerBucketSize = scrollerBucketSize + } + + private fun init(context: Context?) { + detector = GestureDetector(context, this) + scroller = Scroller(context, null, true) + val newScrollAnimator = ValueAnimator.ofFloat(0f, 1f) + newScrollAnimator.addUpdateListener(this) + scrollAnimator = newScrollAnimator + scrollController = object : ScrollController {} + } + + fun reset() { + scroller!!.finalX = 0 + scroller!!.computeScrollOffset() + updateDataOffset() + } + + private fun updateDataOffset() { + var newDataOffset = scroller!!.currX / scrollerBucketSize + newDataOffset = Math.max(0, newDataOffset) + newDataOffset = Math.min(maxDataOffset, newDataOffset) + if (newDataOffset != dataOffset) { + dataOffset = newDataOffset + scrollController!!.onDataOffsetChanged(dataOffset) + postInvalidate() + } + } + + interface ScrollController { + fun onDataOffsetChanged(newDataOffset: Int) {} + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java deleted file mode 100644 index 1ebf43202..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup.LayoutParams; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.core.models.Streak; -import org.isoron.uhabits.core.models.Timestamp; -import org.isoron.uhabits.core.utils.DateUtils; -import org.isoron.uhabits.utils.StyledResources; - -import java.text.DateFormat; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.TimeZone; - -import static android.view.View.MeasureSpec.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class StreakChart extends View -{ - private Paint paint; - - private long minLength; - - private long maxLength; - - private int[] colors; - - private int[] textColors; - - private RectF rect; - - private int baseSize; - - private int primaryColor; - - private List streaks; - - private boolean isBackgroundTransparent; - - private DateFormat dateFormat; - - private int width; - - private float em; - - private float maxLabelWidth; - - private float textMargin; - - private boolean shouldShowLabels; - - private int textColor; - - public StreakChart(Context context) - { - super(context); - init(); - } - - public StreakChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - /** - * Returns the maximum number of streaks this view is able to show, given - * its current size. - * - * @return max number of visible streaks - */ - public int getMaxStreakCount() - { - return (int) Math.floor(getMeasuredHeight() / baseSize); - } - - public void populateWithRandomData() - { - Timestamp start = DateUtils.getToday(); - List streaks = new LinkedList<>(); - - for (int i = 0; i < 10; i++) - { - int length = new Random().nextInt(100); - Timestamp end = start.plus(length); - streaks.add(new Streak(start, end)); - start = end.plus(1); - } - - setStreaks(streaks); - } - - public void setColor(int color) - { - this.primaryColor = color; - postInvalidate(); - } - - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) - { - this.isBackgroundTransparent = isBackgroundTransparent; - initColors(); - } - - public void setStreaks(List streaks) - { - this.streaks = streaks; - initColors(); - updateMaxMinLengths(); - requestLayout(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - if (streaks.size() == 0) return; - - rect.set(0, 0, width, baseSize); - - for (Streak s : streaks) - { - drawRow(canvas, s, rect); - rect.offset(0, baseSize); - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) - { - LayoutParams params = getLayoutParams(); - - if (params != null && params.height == LayoutParams.WRAP_CONTENT) - { - int width = getSize(widthSpec); - int height = streaks.size() * baseSize; - - heightSpec = makeMeasureSpec(height, EXACTLY); - widthSpec = makeMeasureSpec(width, EXACTLY); - } - - setMeasuredDimension(widthSpec, heightSpec); - } - - @Override - protected void onSizeChanged(int width, - int height, - int oldWidth, - int oldHeight) - { - this.width = width; - - Context context = getContext(); - float minTextSize = getDimension(context, R.dimen.tinyTextSize); - float maxTextSize = getDimension(context, R.dimen.regularTextSize); - float textSize = baseSize * 0.5f; - - paint.setTextSize( - Math.max(Math.min(textSize, maxTextSize), minTextSize)); - em = paint.getFontSpacing(); - textMargin = 0.5f * em; - - updateMaxMinLengths(); - } - - private void drawRow(Canvas canvas, Streak streak, RectF rect) - { - if (maxLength == 0) return; - - float percentage = (float) streak.getLength() / maxLength; - float availableWidth = width - 2 * maxLabelWidth; - if (shouldShowLabels) availableWidth -= 2 * textMargin; - - float barWidth = percentage * availableWidth; - float minBarWidth = - paint.measureText(Long.toString(streak.getLength())) + em; - barWidth = Math.max(barWidth, minBarWidth); - - float gap = (width - barWidth) / 2; - float paddingTopBottom = baseSize * 0.05f; - - paint.setColor(percentageToColor(percentage)); - - float round = dpToPixels(getContext(), 2); - canvas.drawRoundRect(rect.left + gap, - rect.top + paddingTopBottom, - rect.right - gap, - rect.bottom - paddingTopBottom, - round, - round, - paint); - - float yOffset = rect.centerY() + 0.3f * em; - - paint.setColor(percentageToTextColor(percentage)); - paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText(Long.toString(streak.getLength()), rect.centerX(), - yOffset, paint); - - if (shouldShowLabels) - { - String startLabel = dateFormat.format(streak.getStart().toJavaDate()); - String endLabel = dateFormat.format(streak.getEnd().toJavaDate()); - - paint.setColor(textColors[1]); - paint.setTextAlign(Paint.Align.RIGHT); - canvas.drawText(startLabel, gap - textMargin, yOffset, paint); - - paint.setTextAlign(Paint.Align.LEFT); - canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint); - } - } - - private void init() - { - initPaints(); - initColors(); - - streaks = Collections.emptyList(); - - dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); - if (!isInEditMode()) dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - rect = new RectF(); - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); - } - - private void initColors() - { - int red = Color.red(primaryColor); - int green = Color.green(primaryColor); - int blue = Color.blue(primaryColor); - - StyledResources res = new StyledResources(getContext()); - - colors = new int[4]; - colors[3] = primaryColor; - colors[2] = Color.argb(192, red, green, blue); - colors[1] = Color.argb(96, red, green, blue); - colors[0] = res.getColor(R.attr.lowContrastTextColor); - - textColors = new int[3]; - textColors[2] = res.getColor(R.attr.highContrastReverseTextColor); - textColors[1] = res.getColor(R.attr.mediumContrastTextColor); - textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor); - } - - private void initPaints() - { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); - } - - private int percentageToColor(float percentage) - { - if (percentage >= 1.0f) return colors[3]; - if (percentage >= 0.8f) return colors[2]; - if (percentage >= 0.5f) return colors[1]; - return colors[0]; - } - - private int percentageToTextColor(float percentage) - { - if (percentage >= 0.5f) return textColors[2]; - return textColors[1]; - } - - private void updateMaxMinLengths() - { - maxLength = 0; - minLength = Long.MAX_VALUE; - shouldShowLabels = true; - - for (Streak s : streaks) - { - maxLength = Math.max(maxLength, s.getLength()); - minLength = Math.min(minLength, s.getLength()); - - float lw1 = - paint.measureText(dateFormat.format(s.getStart().toJavaDate())); - float lw2 = - paint.measureText(dateFormat.format(s.getEnd().toJavaDate())); - maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); - } - - if (width - 2 * maxLabelWidth < width * 0.25f) - { - maxLabelWidth = 0; - shouldShowLabels = false; - } - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt new file mode 100644 index 000000000..098813921 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Streak +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources +import java.text.DateFormat +import java.util.LinkedList +import java.util.Random +import java.util.TimeZone + +class StreakChart : View { + private var paint: Paint? = null + private var minLength: Long = 0 + private var maxLength: Long = 0 + private lateinit var colors: IntArray + private lateinit var textColors: IntArray + private var rect: RectF? = null + private var baseSize = 0 + private var primaryColor = 0 + private var streaks: List? = null + private var isBackgroundTransparent = false + private var dateFormat: DateFormat? = null + private var internalWidth = 0 + private var em = 0f + private var maxLabelWidth = 0f + private var textMargin = 0f + private var shouldShowLabels = false + private val textColor = 0 + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + /** + * Returns the maximum number of streaks this view is able to show, given + * its current size. + * + * @return max number of visible streaks + */ + val maxStreakCount: Int + get() = Math.floor((measuredHeight / baseSize).toDouble()).toInt() + + fun populateWithRandomData() { + var start: Timestamp = getToday() + val streaks: MutableList = LinkedList() + for (i in 0..9) { + val length = Random().nextInt(100) + val end = start.plus(length) + streaks.add(Streak(start, end)) + start = end.plus(1) + } + setStreaks(streaks) + } + + fun setColor(color: Int) { + primaryColor = color + postInvalidate() + } + + fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { + this.isBackgroundTransparent = isBackgroundTransparent + initColors() + } + + fun setStreaks(streaks: List?) { + this.streaks = streaks + initColors() + updateMaxMinLengths() + requestLayout() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (streaks!!.size == 0) return + rect!![0f, 0f, internalWidth.toFloat()] = baseSize.toFloat() + for (s in streaks!!) { + drawRow(canvas, s, rect) + rect!!.offset(0f, baseSize.toFloat()) + } + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + var widthSpec = widthSpec + var heightSpec = heightSpec + val params = layoutParams + if (params != null && params.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + val width = MeasureSpec.getSize(widthSpec) + val height = streaks!!.size * baseSize + heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) + } + setMeasuredDimension(widthSpec, heightSpec) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + this.internalWidth = width + val context = context + val minTextSize = getDimension(context, R.dimen.tinyTextSize) + val maxTextSize = getDimension(context, R.dimen.regularTextSize) + val textSize = baseSize * 0.5f + paint!!.textSize = Math.max(Math.min(textSize, maxTextSize), minTextSize) + em = paint!!.fontSpacing + textMargin = 0.5f * em + updateMaxMinLengths() + } + + private fun drawRow(canvas: Canvas, streak: Streak, rect: RectF?) { + if (maxLength == 0L) return + val percentage = streak.length.toFloat() / maxLength + var availableWidth = internalWidth - 2 * maxLabelWidth + if (shouldShowLabels) availableWidth -= 2 * textMargin + var barWidth = percentage * availableWidth + val minBarWidth = paint!!.measureText(java.lang.Long.toString(streak.length.toLong())) + em + barWidth = Math.max(barWidth, minBarWidth) + val gap = (internalWidth - barWidth) / 2 + val paddingTopBottom = baseSize * 0.05f + paint!!.color = percentageToColor(percentage) + val round = dpToPixels(context, 2f) + canvas.drawRoundRect( + rect!!.left + gap, + rect.top + paddingTopBottom, + rect.right - gap, + rect.bottom - paddingTopBottom, + round, + round, + paint!! + ) + val yOffset = rect.centerY() + 0.3f * em + paint!!.color = percentageToTextColor(percentage) + paint!!.textAlign = Paint.Align.CENTER + canvas.drawText( + java.lang.Long.toString(streak.length.toLong()), + rect.centerX(), + yOffset, + paint!! + ) + if (shouldShowLabels) { + val startLabel = dateFormat!!.format(streak.start.toJavaDate()) + val endLabel = dateFormat!!.format(streak.end.toJavaDate()) + paint!!.color = textColors[1] + paint!!.textAlign = Paint.Align.RIGHT + canvas.drawText(startLabel, gap - textMargin, yOffset, paint!!) + paint!!.textAlign = Paint.Align.LEFT + canvas.drawText(endLabel, internalWidth - gap + textMargin, yOffset, paint!!) + } + } + + private fun init() { + initPaints() + initColors() + streaks = emptyList() + val newDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) + if (!isInEditMode) newDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")) + dateFormat = newDateFormat + rect = RectF() + baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) + } + + private fun initColors() { + val red = Color.red(primaryColor) + val green = Color.green(primaryColor) + val blue = Color.blue(primaryColor) + val res = StyledResources(context) + colors = IntArray(4) + colors[3] = primaryColor + colors[2] = Color.argb(192, red, green, blue) + colors[1] = Color.argb(96, red, green, blue) + colors[0] = res.getColor(R.attr.lowContrastTextColor) + textColors = IntArray(3) + textColors[2] = res.getColor(R.attr.highContrastReverseTextColor) + textColors[1] = res.getColor(R.attr.mediumContrastTextColor) + textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor) + } + + private fun initPaints() { + paint = Paint() + paint!!.textAlign = Paint.Align.CENTER + paint!!.isAntiAlias = true + } + + private fun percentageToColor(percentage: Float): Int { + if (percentage >= 1.0f) return colors[3] + if (percentage >= 0.8f) return colors[2] + return if (percentage >= 0.5f) colors[1] else colors[0] + } + + private fun percentageToTextColor(percentage: Float): Int { + return if (percentage >= 0.5f) textColors[2] else textColors[1] + } + + private fun updateMaxMinLengths() { + maxLength = 0 + minLength = Long.MAX_VALUE + shouldShowLabels = true + for (s in streaks!!) { + maxLength = Math.max(maxLength, s.length.toLong()) + minLength = Math.min(minLength, s.length.toLong()) + val lw1 = paint!!.measureText(dateFormat!!.format(s.start.toJavaDate())) + val lw2 = paint!!.measureText(dateFormat!!.format(s.end.toJavaDate())) + maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)) + } + if (internalWidth - 2 * maxLabelWidth < internalWidth * 0.25f) { + maxLabelWidth = 0f + shouldShowLabels = false + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java deleted file mode 100644 index b9c73bacd..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; -import android.view.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.activities.habits.list.views.*; -import org.isoron.uhabits.utils.*; - -import java.util.*; - -import static android.view.View.MeasureSpec.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class TargetChart extends View -{ - private Paint paint; - private int baseSize; - private int primaryColor; - private int mediumContrastTextColor; - private int highContrastReverseTextColor; - private int lowContrastTextColor; - private RectF rect = new RectF(); - private RectF barRect = new RectF(); - private List values = Collections.emptyList(); - private List labels = Collections.emptyList(); - private List targets = Collections.emptyList(); - private float maxLabelSize; - private float tinyTextSize; - - public TargetChart(Context context) - { - super(context); - init(); - } - - public TargetChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - public void populateWithRandomData() - { - labels = new ArrayList<>(); - values = new ArrayList<>(); - targets = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - double percentage = new Random().nextDouble(); - targets.add(new Random().nextDouble() * 1000.0); - values.add(targets.get(i) * percentage * 1.2); - labels.add(String.format(Locale.US, "Label %d", i + 1)); - } - } - - public void setColor(int color) - { - this.primaryColor = color; - postInvalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - if (labels.size() == 0) return; - - maxLabelSize = 0; - for (String label : labels) { - paint.setTextSize(tinyTextSize); - float len = paint.measureText(label); - maxLabelSize = Math.max(maxLabelSize, len); - } - - float marginTop = (getHeight() - baseSize * labels.size()) / 2.0f; - rect.set(0, marginTop, getWidth(), marginTop + baseSize); - for (int i = 0; i < labels.size(); i++) { - drawRow(canvas, i, rect); - rect.offset(0, baseSize); - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) - { - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); - - int width = getSize(widthSpec); - int height = labels.size() * baseSize; - - ViewGroup.LayoutParams params = getLayoutParams(); - if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) { - height = getSize(heightSpec); - if (labels.size() > 0) baseSize = height / labels.size(); - } - - heightSpec = makeMeasureSpec(height, EXACTLY); - widthSpec = makeMeasureSpec(width, EXACTLY); - setMeasuredDimension(widthSpec, heightSpec); - } - - private void drawRow(Canvas canvas, int row, RectF rect) - { - float padding = dpToPixels(getContext(), 4); - float round = dpToPixels(getContext(), 2); - float stop = maxLabelSize + padding * 2; - - paint.setColor(mediumContrastTextColor); - - // Draw label - paint.setTextSize(tinyTextSize); - paint.setTextAlign(Paint.Align.RIGHT); - float yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f; - canvas.drawText(labels.get(row), - rect.left + stop - padding, - rect.centerY() - yTextAdjust, - paint); - - // Draw background box - paint.setColor(lowContrastTextColor); - barRect.set(rect.left + stop + padding, - rect.top + baseSize * 0.05f, - rect.right - padding, - rect.bottom - baseSize * 0.05f); - canvas.drawRoundRect(barRect, round, round, paint); - - float percentage = (float) (values.get(row) / targets.get(row)); - percentage = Math.min(1.0f, percentage); - - // Draw completed box - float completedWidth = percentage * barRect.width(); - if (completedWidth > 0 && completedWidth < 2 * round) { - completedWidth = 2 * round; - } - float remainingWidth = barRect.width() - completedWidth; - - paint.setColor(primaryColor); - barRect.set(barRect.left, - barRect.top, - barRect.left + completedWidth, - barRect.bottom); - canvas.drawRoundRect(barRect, round, round, paint); - - // Draw values - paint.setColor(Color.WHITE); - paint.setTextSize(tinyTextSize); - paint.setTextAlign(Paint.Align.CENTER); - yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f; - - double remaining = targets.get(row) - values.get(row); - String completedText = NumberButtonViewKt.toShortString(values.get(row)); - String remainingText = NumberButtonViewKt.toShortString(remaining); - - if (completedWidth > paint.measureText(completedText) + 2 * padding) { - paint.setColor(highContrastReverseTextColor); - canvas.drawText(completedText, - barRect.centerX(), - barRect.centerY() - yTextAdjust, - paint); - } - - if (remainingWidth > paint.measureText(remainingText) + 2 * padding) { - paint.setColor(mediumContrastTextColor); - barRect.set(rect.left + stop + padding + completedWidth, - barRect.top, - rect.right - padding, - barRect.bottom); - canvas.drawText(remainingText, - barRect.centerX(), - barRect.centerY() - yTextAdjust, - paint); - } - } - - private void init() - { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); - - StyledResources res = new StyledResources(getContext()); - lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor); - mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor); - highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor); - tinyTextSize = getDimension(getContext(), R.dimen.tinyTextSize); - } - - public void setValues(List values) - { - this.values = values; - requestLayout(); - } - - public void setLabels(List labels) - { - this.labels = labels; - requestLayout(); - } - - public void setTargets(List targets) - { - this.targets = targets; - requestLayout(); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt new file mode 100644 index 000000000..296263f9e --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import org.isoron.uhabits.R +import org.isoron.uhabits.activities.habits.list.views.toShortString +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources + +class TargetChart : View { + private var paint: Paint? = null + private var baseSize = 0 + private var primaryColor = 0 + private var mediumContrastTextColor = 0 + private var highContrastReverseTextColor = 0 + private var lowContrastTextColor = 0 + private val rect = RectF() + private val barRect = RectF() + private var values = emptyList() + private var labels = emptyList() + private var targets = emptyList() + private var maxLabelSize = 0f + private var tinyTextSize = 0f + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + fun setColor(color: Int) { + primaryColor = color + postInvalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (labels.size == 0) return + maxLabelSize = 0f + for (label in labels) { + paint!!.textSize = tinyTextSize + val len = paint!!.measureText(label) + maxLabelSize = Math.max(maxLabelSize, len) + } + val marginTop = (height - baseSize * labels.size) / 2.0f + rect[0f, marginTop, width.toFloat()] = marginTop + baseSize + for (i in labels.indices) { + drawRow(canvas, i, rect) + rect.offset(0f, baseSize.toFloat()) + } + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + var widthSpec = widthSpec + var heightSpec = heightSpec + baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) + val width = MeasureSpec.getSize(widthSpec) + var height = labels.size * baseSize + val params = layoutParams + if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) { + height = MeasureSpec.getSize(heightSpec) + if (labels.size > 0) baseSize = height / labels.size + } + heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) + setMeasuredDimension(widthSpec, heightSpec) + } + + private fun drawRow(canvas: Canvas, row: Int, rect: RectF) { + val padding = dpToPixels(context, 4f) + val round = dpToPixels(context, 2f) + val stop = maxLabelSize + padding * 2 + paint!!.color = mediumContrastTextColor + + // Draw label + paint!!.textSize = tinyTextSize + paint!!.textAlign = Paint.Align.RIGHT + var yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f + canvas.drawText( + labels[row], + rect.left + stop - padding, + rect.centerY() - yTextAdjust, + paint!! + ) + + // Draw background box + paint!!.color = lowContrastTextColor + barRect[rect.left + stop + padding, rect.top + baseSize * 0.05f, rect.right - padding] = + rect.bottom - baseSize * 0.05f + canvas.drawRoundRect(barRect, round, round, paint!!) + var percentage = (values[row] / targets[row]).toFloat() + percentage = Math.min(1.0f, percentage) + + // Draw completed box + var completedWidth = percentage * barRect.width() + if (completedWidth > 0 && completedWidth < 2 * round) { + completedWidth = 2 * round + } + val remainingWidth = barRect.width() - completedWidth + paint!!.color = primaryColor + barRect[barRect.left, barRect.top, barRect.left + completedWidth] = barRect.bottom + canvas.drawRoundRect(barRect, round, round, paint!!) + + // Draw values + paint!!.color = Color.WHITE + paint!!.textSize = tinyTextSize + paint!!.textAlign = Paint.Align.CENTER + yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f + val remaining = targets[row] - values[row] + val completedText = values[row].toShortString() + val remainingText = remaining.toShortString() + if (completedWidth > paint!!.measureText(completedText) + 2 * padding) { + paint!!.color = highContrastReverseTextColor + canvas.drawText( + completedText, + barRect.centerX(), + barRect.centerY() - yTextAdjust, + paint!! + ) + } + if (remainingWidth > paint!!.measureText(remainingText) + 2 * padding) { + paint!!.color = mediumContrastTextColor + barRect[rect.left + stop + padding + completedWidth, barRect.top, rect.right - padding] = + barRect.bottom + canvas.drawText( + remainingText, + barRect.centerX(), + barRect.centerY() - yTextAdjust, + paint!! + ) + } + } + + private fun init() { + paint = Paint() + paint!!.textAlign = Paint.Align.CENTER + paint!!.isAntiAlias = true + val res = StyledResources(context) + lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor) + mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor) + highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor) + tinyTextSize = getDimension(context, R.dimen.tinyTextSize) + } + + fun setValues(values: List) { + this.values = values + requestLayout() + } + + fun setLabels(labels: List) { + this.labels = labels + requestLayout() + } + + fun setTargets(targets: List) { + this.targets = targets + requestLayout() + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt index 3d8ad81c5..56bc7bc9c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt @@ -131,7 +131,7 @@ class HabitCardListView( super.onRestoreInstanceState(state) return } - dataOffset = state.bundle.getInt("dataOffset") + dataOffset = state.bundle!!.getInt("dataOffset") super.onRestoreInstanceState(state.superState) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index cc2f1c254..f54d15e04 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -90,10 +90,12 @@ class HabitCardView( } var score - get() = scoreRing.percentage.toDouble() + get() = scoreRing.getPercentage().toDouble() set(value) { - scoreRing.percentage = value.toFloat() - scoreRing.precision = 1.0f / 16 + // scoreRing.percentage = value.toFloat() + scoreRing.setPercentage(value.toFloat()) + // scoreRing.precision = 1.0f / 16 + scoreRing.setPrecision(1.0f / 16) } var unit @@ -225,7 +227,8 @@ class HabitCardView( setTextColor(c) } scoreRing.apply { - color = c + setColor(c) + // color = c } checkmarkPanel.apply { color = c diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt index ddf657e27..1de75f34a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt @@ -49,8 +49,11 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff) binding.scoreLabel.setTextColor(androidColor) binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100) - binding.scoreRing.color = androidColor - binding.scoreRing.percentage = state.scoreToday + // binding.scoreRing.color = androidColor + binding.scoreRing.setColor(androidColor) + // binding.scoreRing.percentage = state.scoreToday + binding.scoreRing.setPercentage(state.scoreToday) + binding.title.setTextColor(androidColor) binding.totalCountLabel.setTextColor(androidColor) binding.totalCountLabel.text = state.totalCount.toString() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt index e5c740574..fb28bc055 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt @@ -36,7 +36,7 @@ class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex val androidColor = state.color.toThemedAndroidColor(context) binding.title.setTextColor(androidColor) binding.spinner.setSelection(state.spinnerPosition) - binding.scoreView.setScores(state.scores) + binding.scoreView.setScores(state.scores.toMutableList()) binding.scoreView.reset() binding.scoreView.setBucketSize(state.bucketSize) binding.scoreView.setColor(androidColor) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt index 4beeed281..45d78e9f2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt @@ -52,7 +52,7 @@ class ScoreWidget( setIsTransparencyEnabled(true) setBucketSize(viewModel.bucketSize) setColor(habit.color.toThemedAndroidColor(context)) - setScores(viewModel.scores) + setScores(viewModel.scores.toMutableList()) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt index e30dcc22a..effe7f398 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt @@ -83,8 +83,10 @@ class CheckmarkWidgetView : HabitWidgetView { setShadowAlpha(0x00) } } - ring.percentage = percentage - ring.color = fgColor + // ring.percentage = percentage + ring.setPercentage(percentage) + ring.setColor(fgColor) + // ring.color = fgColor ring.setBackgroundColor(bgColor) ring.setText(text) label.text = name From 30630c3358aeadd42541cb3f1927a2cbc7afc39d Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 23:18:50 +0100 Subject: [PATCH 13/35] Convert Timestamp --- .../isoron/uhabits/core/models/Timestamp.java | 159 ------------------ .../isoron/uhabits/core/models/Timestamp.kt | 148 ++++++++++++++++ .../core/preferences/PreferencesTest.kt | 2 +- 3 files changed, 149 insertions(+), 160 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.java deleted file mode 100644 index f67c35ed5..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.models; - -import org.isoron.platform.time.LocalDate; -import org.apache.commons.lang3.builder.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; - -import static java.util.Calendar.*; - -public final class Timestamp implements Comparable { - public static final long DAY_LENGTH = 86400000; - - public static final Timestamp ZERO = new Timestamp(0); - - private final long unixTime; - - public static Timestamp fromLocalDate(LocalDate date) { - return new Timestamp(946684800000L + date.getDaysSince2000() * 86400000L); - } - - public Timestamp(long unixTime) { - if (unixTime < 0) - throw new IllegalArgumentException( - "Invalid unix time: " + unixTime); - - if (unixTime % DAY_LENGTH != 0) - unixTime = (unixTime / DAY_LENGTH) * DAY_LENGTH; - - this.unixTime = unixTime; - } - - public Timestamp(GregorianCalendar cal) { - this(cal.getTimeInMillis()); - } - - public static Timestamp from(int year, int javaMonth, int day) { - GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); - cal.set(year, javaMonth, day, 0, 0, 0); - return new Timestamp(cal.getTimeInMillis()); - } - - public long getUnixTime() { - return unixTime; - } - - public LocalDate toLocalDate() { - long millisSince2000 = unixTime - 946684800000L; - int daysSince2000 = (int) (millisSince2000 / 86400000); - return new LocalDate(daysSince2000); - } - - /** - * Returns -1 if this timestamp is older than the given timestamp, 1 if this - * timestamp is newer, or zero if they are equal. - */ - @Override - public int compareTo(Timestamp other) { - return Long.signum(this.unixTime - other.unixTime); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null || getClass() != o.getClass()) return false; - - Timestamp timestamp = (Timestamp) o; - - return new EqualsBuilder() - .append(unixTime, timestamp.unixTime) - .isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder(17, 37).append(unixTime).toHashCode(); - } - - /** - * Given two timestamps, returns whichever timestamp is the oldest one. - */ - public static Timestamp oldest(Timestamp first, Timestamp second) { - return first.unixTime < second.unixTime ? first : second; - } - - public Timestamp minus(int days) { - return plus(-days); - } - - public Timestamp plus(int days) { - return new Timestamp(unixTime + DAY_LENGTH * days); - } - - /** - * Returns the number of days between this timestamp and the given one. If - * the other timestamp equals this one, returns zero. If the other timestamp - * is older than this one, returns a negative number. - */ - public int daysUntil(Timestamp other) { - return (int) ((other.unixTime - this.unixTime) / DAY_LENGTH); - } - - public boolean isNewerThan(Timestamp other) { - return compareTo(other) > 0; - } - - public boolean isOlderThan(Timestamp other) { - return compareTo(other) < 0; - } - - - public Date toJavaDate() { - return new Date(unixTime); - } - - public GregorianCalendar toCalendar() { - GregorianCalendar day = - new GregorianCalendar(TimeZone.getTimeZone("GMT")); - day.setTimeInMillis(unixTime); - return day; - } - - @Override - public String toString() { - return DateFormats.getCSVDateFormat().format(new Date(unixTime)); - } - - /** - * Returns an integer corresponding to the day of the week. Saturday maps - * to 0, Sunday maps to 1, and so on. - */ - public int getWeekday() { - return toCalendar().get(DAY_OF_WEEK) % 7; - } - - Timestamp truncate(DateUtils.TruncateField field, int firstWeekday) { - return new Timestamp(DateUtils.truncate(field, unixTime, firstWeekday)); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt new file mode 100644 index 000000000..d50f5d52d --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.models + +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.isoron.platform.time.LocalDate +import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat +import org.isoron.uhabits.core.utils.DateUtils +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar +import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar +import java.util.TimeZone + +class Timestamp(unixTime: Long) : Comparable { + val unixTime: Long + + constructor(cal: GregorianCalendar) : this(cal.timeInMillis) {} + + fun toLocalDate(): LocalDate { + val millisSince2000 = unixTime - 946684800000L + val daysSince2000 = (millisSince2000 / 86400000).toInt() + return LocalDate(daysSince2000) + } + + /** + * Returns -1 if this timestamp is older than the given timestamp, 1 if this + * timestamp is newer, or zero if they are equal. + */ + override fun compareTo(other: Timestamp): Int { + return java.lang.Long.signum(unixTime - other.unixTime) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val timestamp = other as Timestamp + return EqualsBuilder() + .append(unixTime, timestamp.unixTime) + .isEquals + } + + override fun hashCode(): Int { + return HashCodeBuilder(17, 37).append(unixTime).toHashCode() + } + + operator fun minus(days: Int): Timestamp { + return plus(-days) + } + + operator fun plus(days: Int): Timestamp { + return Timestamp(unixTime + DAY_LENGTH * days) + } + + /** + * Returns the number of days between this timestamp and the given one. If + * the other timestamp equals this one, returns zero. If the other timestamp + * is older than this one, returns a negative number. + */ + fun daysUntil(other: Timestamp): Int { + return ((other.unixTime - unixTime) / DAY_LENGTH).toInt() + } + + fun isNewerThan(other: Timestamp): Boolean { + return compareTo(other) > 0 + } + + fun isOlderThan(other: Timestamp): Boolean { + return compareTo(other) < 0 + } + + fun toJavaDate(): Date { + return Date(unixTime) + } + + fun toCalendar(): GregorianCalendar { + val day = GregorianCalendar(TimeZone.getTimeZone("GMT")) + day.timeInMillis = unixTime + return day + } + + override fun toString(): String { + return getCSVDateFormat().format(Date(unixTime)) + } + + /** + * Returns an integer corresponding to the day of the week. Saturday maps + * to 0, Sunday maps to 1, and so on. + */ + val weekday: Int + get() = toCalendar()[Calendar.DAY_OF_WEEK] % 7 + + fun truncate(field: DateUtils.TruncateField?, firstWeekday: Int): Timestamp { + return Timestamp( + truncate( + field!!, + unixTime, + firstWeekday + ) + ) + } + + companion object { + const val DAY_LENGTH: Long = 86400000 + val ZERO = Timestamp(0) + fun fromLocalDate(date: LocalDate): Timestamp { + return Timestamp(946684800000L + date.daysSince2000 * 86400000L) + } + + fun from(year: Int, javaMonth: Int, day: Int): Timestamp { + val cal = getStartOfTodayCalendar() + cal[year, javaMonth, day, 0, 0] = 0 + return Timestamp(cal.timeInMillis) + } + + /** + * Given two timestamps, returns whichever timestamp is the oldest one. + */ + fun oldest(first: Timestamp, second: Timestamp): Timestamp { + return if (first.unixTime < second.unixTime) first else second + } + } + + init { + var unixTime = unixTime + require(unixTime >= 0) { "Invalid unix time: $unixTime" } + if (unixTime % DAY_LENGTH != 0L) unixTime = unixTime / DAY_LENGTH * DAY_LENGTH + this.unixTime = unixTime + } +} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt index 340f97440..d73cf9bce 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt @@ -26,7 +26,7 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.HabitList -import org.isoron.uhabits.core.models.Timestamp.ZERO +import org.isoron.uhabits.core.models.Timestamp.Companion.ZERO import org.isoron.uhabits.core.ui.ThemeSwitcher import org.junit.Before import org.junit.Test From a58cbffb8146a3fb5535dea5e777beab10b81952 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 23:25:11 +0100 Subject: [PATCH 14/35] Convert HabitList and ModelObservable --- .../models/{HabitList.java => HabitList.kt} | 206 +++++++----------- ...odelObservable.java => ModelObservable.kt} | 58 +++-- 2 files changed, 111 insertions(+), 153 deletions(-) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/{HabitList.java => HabitList.kt} (56%) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/{ModelObservable.java => ModelObservable.kt} (73%) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt similarity index 56% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 2ca00c56b..455518e7a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -16,51 +16,45 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.models -package org.isoron.uhabits.core.models; - -import androidx.annotation.*; - -import com.opencsv.*; - -import java.io.*; -import java.util.*; - -import javax.annotation.concurrent.*; +import com.opencsv.CSVWriter +import java.io.IOException +import java.io.Writer +import java.util.LinkedList +import javax.annotation.concurrent.ThreadSafe /** - * An ordered collection of {@link Habit}s. + * An ordered collection of [Habit]s. */ @ThreadSafe -public abstract class HabitList implements Iterable -{ - private final ModelObservable observable; - - @NonNull - protected final HabitMatcher filter; +abstract class HabitList : Iterable { + val observable: ModelObservable + @JvmField + protected val filter: HabitMatcher /** * Creates a new HabitList. - *

+ * + * * Depending on the implementation, this list can either be empty or be * populated by some pre-existing habits, for example, from a certain * database. */ - public HabitList() - { - observable = new ModelObservable(); - filter = new HabitMatcherBuilder().setArchivedAllowed(true).build(); + constructor() { + observable = ModelObservable() + filter = HabitMatcherBuilder().setArchivedAllowed(true).build() } - protected HabitList(@NonNull HabitMatcher filter) - { - observable = new ModelObservable(); - this.filter = filter; + protected constructor(filter: HabitMatcher) { + observable = ModelObservable() + this.filter = filter } /** * Inserts a new habit in the list. - *

+ * + * * If the id of the habit is null, the list will assign it a new id, which * is guaranteed to be unique in the scope of the list. If id is not null, * the caller should make sure that the list does not already contain @@ -69,8 +63,8 @@ public abstract class HabitList implements Iterable * @param habit the habit to be inserted * @throws IllegalArgumentException if the habit is already on the list. */ - public abstract void add(@NonNull Habit habit) - throws IllegalArgumentException; + @Throws(IllegalArgumentException::class) + abstract fun add(habit: Habit) /** * Returns the habit with specified id. @@ -78,8 +72,7 @@ public abstract class HabitList implements Iterable * @param id the id of the habit * @return the habit, or null if none exist */ - @Nullable - public abstract Habit getById(long id); + abstract fun getById(id: Long): Habit? /** * Returns the habit with specified UUID. @@ -87,8 +80,7 @@ public abstract class HabitList implements Iterable * @param uuid the UUID of the habit * @return the habit, or null if none exist */ - @Nullable - public abstract Habit getByUUID(String uuid); + abstract fun getByUUID(uuid: String?): Habit? /** * Returns the habit that occupies a certain position. @@ -97,8 +89,7 @@ public abstract class HabitList implements Iterable * @return the habit at that position * @throws IndexOutOfBoundsException when the position is invalid */ - @NonNull - public abstract Habit getByPosition(int position); + abstract fun getByPosition(position: Int): Habit /** * Returns the list of habits that match a given condition. @@ -106,31 +97,23 @@ public abstract class HabitList implements Iterable * @param matcher the matcher that checks the condition * @return the list of matching habits */ - @NonNull - public abstract HabitList getFiltered(HabitMatcher matcher); + abstract fun getFiltered(matcher: HabitMatcher?): HabitList + abstract var primaryOrder: Order + abstract var secondaryOrder: Order - public ModelObservable getObservable() - { - return observable; - } - - public abstract Order getPrimaryOrder(); + // /** + // * Changes the order of the elements on the list. + // * + // * @param order the new order criterion + // */ + // abstract fun setPrimaryOrder(order: Order) - public abstract Order getSecondaryOrder(); - - /** - * Changes the order of the elements on the list. - * - * @param order the new order criterion - */ - public abstract void setPrimaryOrder(@NonNull Order order); - - /** - * Changes the previous order of the elements on the list. - * - * @param order the new order criterion - */ - public abstract void setSecondaryOrder(@NonNull Order order); + // /** + // * Changes the previous order of the elements on the list. + // * + // * @param order the new order criterion + // */ + // abstract fun setSecondaryOrder(order: Order) /** * Returns the index of the given habit in the list, or -1 if the list does @@ -139,31 +122,28 @@ public abstract class HabitList implements Iterable * @param h the habit * @return the index of the habit, or -1 if not in the list */ - public abstract int indexOf(@NonNull Habit h); - - public boolean isEmpty() - { - return size() == 0; - } + abstract fun indexOf(h: Habit): Int + val isEmpty: Boolean + get() = size() == 0 /** * Removes the given habit from the list. - *

+ * + * * If the given habit is not in the list, does nothing. * * @param h the habit to be removed. */ - public abstract void remove(@NonNull Habit h); + abstract fun remove(h: Habit) /** * Removes all the habits from the list. */ - public void removeAll() - { - List copy = new LinkedList<>(); - for (Habit h : this) copy.add(h); - for (Habit h : copy) remove(h); - observable.notifyListeners(); + open fun removeAll() { + val copy: MutableList = LinkedList() + for (h in this) copy.add(h) + for (h in copy) remove(h) + observable.notifyListeners() } /** @@ -172,40 +152,38 @@ public abstract class HabitList implements Iterable * @param from the habit that should be moved * @param to the habit that currently occupies the desired position */ - public abstract void reorder(@NonNull Habit from, @NonNull Habit to); - - public void repair() - { - } + abstract fun reorder(from: Habit, to: Habit) + open fun repair() {} /** * Returns the number of habits in this list. * * @return number of habits */ - public abstract int size(); + abstract fun size(): Int /** * Notifies the list that a certain list of habits has been modified. - *

+ * + * * Depending on the implementation, this operation might trigger a write to * disk, or do nothing at all. To make sure that the habits get persisted, * this operation must be called. * * @param habits the list of habits that have been modified. */ - public abstract void update(List habits); + abstract fun update(habits: List?) /** * Notifies the list that a certain habit has been modified. - *

- * See {@link #update(List)} for more details. + * + * + * See [.update] for more details. * * @param habit the habit that has been modified. */ - public void update(@NonNull Habit habit) - { - update(Collections.singletonList(habit)); + fun update(habit: Habit) { + update(listOf(habit)) } /** @@ -217,9 +195,9 @@ public abstract class HabitList implements Iterable * @param out the writer that will receive the result * @throws IOException if write operations fail */ - public void writeCSV(@NonNull Writer out) throws IOException - { - String[] header = { + @Throws(IOException::class) + fun writeCSV(out: Writer) { + val header = arrayOf( "Position", "Name", "Question", @@ -227,43 +205,27 @@ public abstract class HabitList implements Iterable "NumRepetitions", "Interval", "Color" - }; - - CSVWriter csv = new CSVWriter(out); - csv.writeNext(header, false); - - for (Habit habit : this) - { - Frequency freq = habit.getFrequency(); - - String[] cols = { + ) + val csv = CSVWriter(out) + csv.writeNext(header, false) + for (habit in this) { + val (numerator, denominator) = habit.frequency + val cols = arrayOf( String.format("%03d", indexOf(habit) + 1), - habit.getName(), - habit.getQuestion(), - habit.getDescription(), - Integer.toString(freq.getNumerator()), - Integer.toString(freq.getDenominator()), - habit.getColor().toCsvColor(), - }; - - csv.writeNext(cols, false); + habit.name, + habit.question, + habit.description, + numerator.toString(), + denominator.toString(), + habit.color.toCsvColor() + ) + csv.writeNext(cols, false) } - - csv.close(); + csv.close() } - public abstract void resort(); - - public enum Order - { - BY_NAME_ASC, - BY_NAME_DESC, - BY_COLOR_ASC, - BY_COLOR_DESC, - BY_SCORE_ASC, - BY_SCORE_DESC, - BY_STATUS_ASC, - BY_STATUS_DESC, - BY_POSITION + abstract fun resort() + enum class Order { + BY_NAME_ASC, BY_NAME_DESC, BY_COLOR_ASC, BY_COLOR_DESC, BY_SCORE_ASC, BY_SCORE_DESC, BY_STATUS_ASC, BY_STATUS_DESC, BY_POSITION } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt similarity index 73% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt index ffe0d6d6e..cf472122a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt @@ -16,74 +16,70 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.models -package org.isoron.uhabits.core.models; - -import java.util.*; - -import javax.annotation.concurrent.*; +import java.util.LinkedList +import javax.annotation.concurrent.ThreadSafe /** * A ModelObservable allows objects to subscribe themselves to it and receive * notifications whenever the model is changed. */ @ThreadSafe -public class ModelObservable -{ - private List listeners; - - /** - * Creates a new ModelObservable with no listeners. - */ - public ModelObservable() - { - super(); - listeners = new LinkedList<>(); - } +class ModelObservable { + private val listeners: MutableList /** * Adds the given listener to the observable. * * @param l the listener to be added. */ - public synchronized void addListener(Listener l) - { - listeners.add(l); + @Synchronized + fun addListener(l: Listener) { + listeners.add(l) } /** * Notifies every listener that the model has changed. - *

+ * + * * Only models should call this method. */ - public synchronized void notifyListeners() - { - for (Listener l : listeners) l.onModelChange(); + @Synchronized + fun notifyListeners() { + for (l in listeners) l.onModelChange() } /** * Removes the given listener. - *

+ * + * * The listener will no longer be notified when the model changes. If the * given listener is not subscribed to this observable, does nothing. * * @param l the listener to be removed */ - public synchronized void removeListener(Listener l) - { - listeners.remove(l); + @Synchronized + fun removeListener(l: Listener) { + listeners.remove(l) } /** * Interface implemented by objects that want to be notified when the model * changes. */ - public interface Listener - { + interface Listener { /** * Called whenever the model associated to this observable has been * modified. */ - void onModelChange(); + fun onModelChange() + } + + /** + * Creates a new ModelObservable with no listeners. + */ + init { + listeners = LinkedList() } } From 1cec5a60675da535c0e1cc998582f6dd3411c074 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 23:30:19 +0100 Subject: [PATCH 15/35] Convert model memory --- .../uhabits/core/models/ModelObservable.kt | 2 +- .../core/models/memory/MemoryHabitList.java | 297 ------------------ .../core/models/memory/MemoryHabitList.kt | 233 ++++++++++++++ 3 files changed, 234 insertions(+), 298 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt index cf472122a..bbe27fd3b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt @@ -68,7 +68,7 @@ class ModelObservable { * Interface implemented by objects that want to be notified when the model * changes. */ - interface Listener { + fun interface Listener { /** * Called whenever the model associated to this observable has been * modified. diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java deleted file mode 100644 index b755c7588..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.models.memory; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -import static org.isoron.uhabits.core.models.HabitList.Order.*; - -/** - * In-memory implementation of {@link HabitList}. - */ -public class MemoryHabitList extends HabitList -{ - @NonNull - private LinkedList list = new LinkedList<>(); - - @NonNull - private Order primaryOrder = Order.BY_POSITION; - - @NonNull - private Order secondaryOrder = Order.BY_NAME_ASC; - - private Comparator comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder); - - @Nullable - private MemoryHabitList parent = null; - - public MemoryHabitList() - { - super(); - } - - protected MemoryHabitList(@NonNull HabitMatcher matcher, - Comparator comparator, - @NonNull MemoryHabitList parent) - { - super(matcher); - this.parent = parent; - this.comparator = comparator; - this.primaryOrder = parent.primaryOrder; - this.secondaryOrder = parent.secondaryOrder; - parent.getObservable().addListener(this::loadFromParent); - loadFromParent(); - } - - @Override - public synchronized void add(@NonNull Habit habit) - throws IllegalArgumentException - { - throwIfHasParent(); - if (list.contains(habit)) - throw new IllegalArgumentException("habit already added"); - - Long id = habit.getId(); - if (id != null && getById(id) != null) - throw new RuntimeException("duplicate id"); - - if (id == null) habit.setId((long) list.size()); - list.addLast(habit); - resort(); - } - - @Override - public synchronized Habit getById(long id) - { - for (Habit h : list) - { - if (h.getId() == null) throw new IllegalStateException(); - if (h.getId() == id) return h; - } - return null; - } - - @Override - public synchronized Habit getByUUID(String uuid) - { - for (Habit h : list) if (Objects.requireNonNull(h.getUuid()).equals(uuid)) return h; - return null; - } - - @NonNull - @Override - public synchronized Habit getByPosition(int position) - { - return list.get(position); - } - - @NonNull - @Override - public synchronized HabitList getFiltered(HabitMatcher matcher) - { - return new MemoryHabitList(matcher, comparator, this); - } - - @NotNull - @Override - public synchronized Order getPrimaryOrder() - { - return primaryOrder; - } - - @NotNull - @Override - public synchronized Order getSecondaryOrder() - { - return secondaryOrder; - } - - @Override - public synchronized void setPrimaryOrder(@NonNull Order order) - { - this.primaryOrder = order; - this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder); - resort(); - } - - @Override - public void setSecondaryOrder(@NonNull Order order) - { - this.secondaryOrder = order; - this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder); - resort(); - } - - private Comparator getComposedComparatorByOrder(Order firstOrder, Order secondOrder) - { - return (h1, h2) -> { - int firstResult = getComparatorByOrder(firstOrder).compare(h1, h2); - - if (firstResult != 0 || secondOrder == null) { - return firstResult; - } - - return getComparatorByOrder(secondOrder).compare(h1, h2); - }; - } - - private Comparator getComparatorByOrder(Order order) { - Comparator nameComparatorAsc = (h1, h2) -> - h1.getName().compareTo(h2.getName()); - - Comparator nameComparatorDesc = (h1, h2) -> - nameComparatorAsc.compare(h2, h1); - - Comparator colorComparatorAsc = (h1, h2) -> - h1.getColor().compareTo(h2.getColor()); - - Comparator colorComparatorDesc = (h1, h2) -> - colorComparatorAsc.compare(h2, h1); - - Comparator scoreComparatorDesc = (h1, h2) -> - { - Timestamp today = DateUtils.getTodayWithOffset(); - return Double.compare( - h1.getScores().get(today).getValue(), - h2.getScores().get(today).getValue()); - }; - - Comparator scoreComparatorAsc = (h1, h2) -> - scoreComparatorDesc.compare(h2, h1); - - Comparator positionComparator = (h1, h2) -> - Integer.compare(h1.getPosition(), h2.getPosition()); - - Comparator statusComparatorDesc = (h1, h2) -> - { - if (h1.isCompletedToday() != h2.isCompletedToday()) { - return h1.isCompletedToday() ? -1 : 1; - } - - if (h1.isNumerical() != h2.isNumerical()) { - return h1.isNumerical() ? -1 : 1; - } - - Timestamp today = DateUtils.getTodayWithOffset(); - Integer v1 = h1.getComputedEntries().get(today).getValue(); - Integer v2 = h2.getComputedEntries().get(today).getValue(); - - return v2.compareTo(v1); - }; - - Comparator statusComparatorAsc = (h1, h2) -> statusComparatorDesc.compare(h2, h1); - - if (order == BY_POSITION) return positionComparator; - if (order == BY_NAME_ASC) return nameComparatorAsc; - if (order == BY_NAME_DESC) return nameComparatorDesc; - if (order == BY_COLOR_ASC) return colorComparatorAsc; - if (order == BY_COLOR_DESC) return colorComparatorDesc; - if (order == BY_SCORE_DESC) return scoreComparatorDesc; - if (order == BY_SCORE_ASC) return scoreComparatorAsc; - if (order == BY_STATUS_DESC) return statusComparatorDesc; - if (order == BY_STATUS_ASC) return statusComparatorAsc; - throw new IllegalStateException(); - } - - @Override - public synchronized int indexOf(@NonNull Habit h) - { - return list.indexOf(h); - } - - @NonNull - @Override - public synchronized Iterator iterator() - { - return new ArrayList<>(list).iterator(); - } - - @Override - public synchronized void remove(@NonNull Habit habit) - { - throwIfHasParent(); - list.remove(habit); - getObservable().notifyListeners(); - } - - @Override - public synchronized void reorder(@NonNull Habit from, @NonNull Habit to) - { - throwIfHasParent(); - if (primaryOrder != BY_POSITION) throw new IllegalStateException( - "cannot reorder automatically sorted list"); - - if (indexOf(from) < 0) throw new IllegalArgumentException( - "list does not contain (from) habit"); - - int toPos = indexOf(to); - if (toPos < 0) throw new IllegalArgumentException( - "list does not contain (to) habit"); - - list.remove(from); - list.add(toPos, from); - - int position = 0; - for(Habit h : list) - h.setPosition(position++); - - getObservable().notifyListeners(); - } - - @Override - public synchronized int size() - { - return list.size(); - } - - @Override - public synchronized void update(List habits) - { - resort(); - } - - private void throwIfHasParent() - { - if (parent != null) throw new IllegalStateException( - "Filtered lists cannot be modified directly. " + - "You should modify the parent list instead."); - } - - private synchronized void loadFromParent() - { - if (parent == null) throw new IllegalStateException(); - - list.clear(); - for (Habit h : parent) if (filter.matches(h)) list.add(h); - resort(); - } - - public synchronized void resort() - { - if (comparator != null) list.sort(comparator); - getObservable().notifyListeners(); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt new file mode 100644 index 000000000..72447862f --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.models.memory + +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.ArrayList +import java.util.Comparator +import java.util.LinkedList +import java.util.Objects + +/** + * In-memory implementation of [HabitList]. + */ +class MemoryHabitList : HabitList { + private val list = LinkedList() + + @get:Synchronized + override var primaryOrder = Order.BY_POSITION + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + @get:Synchronized + override var secondaryOrder = Order.BY_NAME_ASC + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + private var comparator: Comparator? = + getComposedComparatorByOrder(primaryOrder, secondaryOrder) + private var parent: MemoryHabitList? = null + + constructor() : super() + constructor( + matcher: HabitMatcher, + comparator: Comparator?, + parent: MemoryHabitList + ) : super(matcher) { + this.parent = parent + this.comparator = comparator + primaryOrder = parent.primaryOrder + secondaryOrder = parent.secondaryOrder + parent.observable.addListener { loadFromParent() } + loadFromParent() + } + + @Synchronized + @Throws(IllegalArgumentException::class) + override fun add(habit: Habit) { + throwIfHasParent() + require(!list.contains(habit)) { "habit already added" } + val id = habit.id + if (id != null && getById(id) != null) throw RuntimeException("duplicate id") + if (id == null) habit.id = list.size.toLong() + list.addLast(habit) + resort() + } + + @Synchronized + override fun getById(id: Long): Habit? { + for (h in list) { + checkNotNull(h.id) + if (h.id == id) return h + } + return null + } + + @Synchronized + override fun getByUUID(uuid: String?): Habit? { + for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h + return null + } + + @Synchronized + override fun getByPosition(position: Int): Habit { + return list[position] + } + + @Synchronized + override fun getFiltered(matcher: HabitMatcher?): HabitList { + return MemoryHabitList(matcher!!, comparator, this) + } + + private fun getComposedComparatorByOrder( + firstOrder: Order, + secondOrder: Order? + ): Comparator { + return Comparator { h1: Habit, h2: Habit -> + val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2) + if (firstResult != 0 || secondOrder == null) { + return@Comparator firstResult + } + getComparatorByOrder(secondOrder).compare(h1, h2) + } + } + + private fun getComparatorByOrder(order: Order): Comparator { + val nameComparatorAsc = Comparator { habit1, habit2 -> + habit1.name.compareTo( + habit2.name + ) + } + val nameComparatorDesc = + Comparator { h1: Habit, h2: Habit -> nameComparatorAsc.compare(h2, h1) } + val colorComparatorAsc = Comparator { (color1), (color2) -> + color1.compareTo( + color2 + ) + } + val colorComparatorDesc = + Comparator { h1: Habit, h2: Habit -> colorComparatorAsc.compare(h2, h1) } + val scoreComparatorDesc = + Comparator { habit1, habit2 -> + val today = getTodayWithOffset() + habit1.scores[today].value.compareTo(habit2.scores[today].value) + } + val scoreComparatorAsc = + Comparator { h1: Habit, h2: Habit -> scoreComparatorDesc.compare(h2, h1) } + val positionComparator = + Comparator { habit1, habit2 -> + habit1.position.compareTo(habit2.position) + } + val statusComparatorDesc = Comparator { h1: Habit, h2: Habit -> + if (h1.isCompletedToday() != h2.isCompletedToday()) { + return@Comparator if (h1.isCompletedToday()) -1 else 1 + } + if (h1.isNumerical != h2.isNumerical) { + return@Comparator if (h1.isNumerical) -1 else 1 + } + val today = getTodayWithOffset() + val v1 = h1.computedEntries.get(today).value + val v2 = h2.computedEntries.get(today).value + v2.compareTo(v1) + } + val statusComparatorAsc = + Comparator { h1: Habit, h2: Habit -> statusComparatorDesc.compare(h2, h1) } + if (order === Order.BY_POSITION) return positionComparator + if (order === Order.BY_NAME_ASC) return nameComparatorAsc + if (order === Order.BY_NAME_DESC) return nameComparatorDesc + if (order === Order.BY_COLOR_ASC) return colorComparatorAsc + if (order === Order.BY_COLOR_DESC) return colorComparatorDesc + if (order === Order.BY_SCORE_DESC) return scoreComparatorDesc + if (order === Order.BY_SCORE_ASC) return scoreComparatorAsc + if (order === Order.BY_STATUS_DESC) return statusComparatorDesc + if (order === Order.BY_STATUS_ASC) return statusComparatorAsc + throw IllegalStateException() + } + + @Synchronized + override fun indexOf(h: Habit): Int { + return list.indexOf(h) + } + + @Synchronized + override fun iterator(): Iterator { + return ArrayList(list).iterator() + } + + @Synchronized + override fun remove(h: Habit) { + throwIfHasParent() + list.remove(h) + observable.notifyListeners() + } + + @Synchronized + override fun reorder(from: Habit, to: Habit) { + throwIfHasParent() + check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" } + require(indexOf(from) >= 0) { "list does not contain (from) habit" } + val toPos = indexOf(to) + require(toPos >= 0) { "list does not contain (to) habit" } + list.remove(from) + list.add(toPos, from) + var position = 0 + for (h in list) h.position = position++ + observable.notifyListeners() + } + + @Synchronized + override fun size(): Int { + return list.size + } + + @Synchronized + override fun update(habits: List?) { + resort() + } + + private fun throwIfHasParent() { + check(parent == null) { + "Filtered lists cannot be modified directly. " + + "You should modify the parent list instead." + } + } + + @Synchronized + private fun loadFromParent() { + checkNotNull(parent) + list.clear() + for (h in parent!!) if (filter.matches(h)) list.add(h) + resort() + } + + @Synchronized + override fun resort() { + if (comparator != null) list.sortWith(comparator!!) + observable.notifyListeners() + } +} From a7df0bde3e119532fe5ffd8ff187137270ed9974 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 23:36:38 +0100 Subject: [PATCH 16/35] Convert core.models.sqlite --- .../isoron/uhabits/core/io/LoopDBImporter.kt | 4 +- .../core/models/sqlite/SQLiteHabitList.java | 296 ------------------ .../core/models/sqlite/SQLiteHabitList.kt | 226 +++++++++++++ .../{EntryRecord.java => EntryRecord.kt} | 50 +-- .../models/sqlite/records/HabitRecord.java | 141 --------- .../core/models/sqlite/records/HabitRecord.kt | 138 ++++++++ 6 files changed, 392 insertions(+), 463 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/{EntryRecord.java => EntryRecord.kt} (51%) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt index c8f76e4a5..0969cd849 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt @@ -100,9 +100,9 @@ class LoopDBImporter habit = habitList.getByUUID(habitRecord.uuid) for (r in entryRecords) { - val t = Timestamp(r.timestamp) + val t = Timestamp(r.timestamp!!) val (_, value) = habit!!.originalEntries.get(t) - if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value).run() + if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value!!).run() } runner.notifyListeners(command) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java deleted file mode 100644 index 4e8426a7d..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.models.sqlite; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.models.memory.*; -import org.isoron.uhabits.core.models.sqlite.records.*; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -import javax.inject.*; - -/** - * Implementation of a {@link HabitList} that is backed by SQLite. - */ -public class SQLiteHabitList extends HabitList -{ - @NonNull - private final Repository repository; - - @NonNull - private final ModelFactory modelFactory; - - @NonNull - private final MemoryHabitList list; - - private boolean loaded = false; - - @Inject - public SQLiteHabitList(@NonNull ModelFactory modelFactory) - { - super(); - this.modelFactory = modelFactory; - this.list = new MemoryHabitList(); - this.repository = modelFactory.buildHabitListRepository(); - } - - private void loadRecords() - { - if (loaded) return; - loaded = true; - - list.removeAll(); - List records = repository.findAll("order by position"); - - int expectedPosition = 0; - boolean shouldRebuildOrder = false; - for (HabitRecord rec : records) - { - if (rec.position != expectedPosition) shouldRebuildOrder = true; - expectedPosition++; - - Habit h = modelFactory.buildHabit(); - rec.copyTo(h); - ((SQLiteEntryList) h.getOriginalEntries()).setHabitId(h.getId()); - list.add(h); - } - - if(shouldRebuildOrder) rebuildOrder(); - } - - @Override - public synchronized void add(@NonNull Habit habit) - { - loadRecords(); - habit.setPosition(size()); - - HabitRecord record = new HabitRecord(); - record.copyFrom(habit); - repository.save(record); - habit.setId(record.id); - ((SQLiteEntryList) habit.getOriginalEntries()).setHabitId(record.id); - - list.add(habit); - getObservable().notifyListeners(); - } - - @Override - @Nullable - public synchronized Habit getById(long id) - { - loadRecords(); - return list.getById(id); - } - - @Override - @Nullable - public synchronized Habit getByUUID(String uuid) - { - loadRecords(); - return list.getByUUID(uuid); - } - - @Override - @NonNull - public synchronized Habit getByPosition(int position) - { - loadRecords(); - return list.getByPosition(position); - } - - @NonNull - @Override - public synchronized HabitList getFiltered(HabitMatcher filter) - { - loadRecords(); - return list.getFiltered(filter); - } - - @Override - @NonNull - public Order getPrimaryOrder() - { - return list.getPrimaryOrder(); - } - - @Override - public Order getSecondaryOrder() - { - return list.getSecondaryOrder(); - } - - @Override - public synchronized void setPrimaryOrder(@NonNull Order order) - { - list.setPrimaryOrder(order); - getObservable().notifyListeners(); - } - - @Override - public synchronized void setSecondaryOrder(@NonNull Order order) - { - list.setSecondaryOrder(order); - getObservable().notifyListeners(); - } - - @Override - public synchronized int indexOf(@NonNull Habit h) - { - loadRecords(); - return list.indexOf(h); - } - - @NotNull - @Override - public synchronized Iterator iterator() - { - loadRecords(); - return list.iterator(); - } - - private synchronized void rebuildOrder() - { - List records = repository.findAll("order by position"); - repository.executeAsTransaction(() -> - { - int pos = 0; - for (HabitRecord r : records) - { - if (r.position != pos) - { - r.position = pos; - repository.save(r); - } - pos++; - } - }); - } - - @Override - public synchronized void remove(@NonNull Habit habit) - { - loadRecords(); - - reorder(habit, list.getByPosition(size() - 1)); - - list.remove(habit); - - HabitRecord record = repository.find(habit.getId()); - if (record == null) throw new RuntimeException("habit not in database"); - repository.executeAsTransaction(() -> - { - habit.getOriginalEntries().clear(); - repository.remove(record); - }); - - getObservable().notifyListeners(); - } - - @Override - public synchronized void removeAll() - { - list.removeAll(); - repository.execSQL("delete from habits"); - repository.execSQL("delete from repetitions"); - getObservable().notifyListeners(); - } - - @Override - public synchronized void reorder(@NonNull Habit from, @NonNull Habit to) - { - loadRecords(); - list.reorder(from, to); - - HabitRecord fromRecord = repository.find(from.getId()); - HabitRecord toRecord = repository.find(to.getId()); - - if (fromRecord == null) - throw new RuntimeException("habit not in database"); - if (toRecord == null) - throw new RuntimeException("habit not in database"); - - if (toRecord.position < fromRecord.position) - { - repository.execSQL("update habits set position = position + 1 " + - "where position >= ? and position < ?", - toRecord.position, fromRecord.position); - } - else - { - repository.execSQL("update habits set position = position - 1 " + - "where position > ? and position <= ?", - fromRecord.position, toRecord.position); - } - - fromRecord.position = toRecord.position; - repository.save(fromRecord); - - getObservable().notifyListeners(); - } - - @Override - public synchronized void repair() - { - loadRecords(); - rebuildOrder(); - getObservable().notifyListeners(); - } - - @Override - public synchronized int size() - { - loadRecords(); - return list.size(); - } - - @Override - public synchronized void update(List habits) - { - loadRecords(); - list.update(habits); - - for (Habit h : habits) - { - HabitRecord record = repository.find(h.getId()); - if (record == null) continue; - record.copyFrom(h); - repository.save(record); - } - - getObservable().notifyListeners(); - } - - @Override - public void resort() - { - list.resort(); - getObservable().notifyListeners(); - } - - public synchronized void reload() - { - loaded = false; - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt new file mode 100644 index 000000000..acb32141b --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.models.sqlite + +import org.isoron.uhabits.core.database.Repository +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.models.ModelFactory +import org.isoron.uhabits.core.models.memory.MemoryHabitList +import org.isoron.uhabits.core.models.sqlite.records.HabitRecord +import javax.inject.Inject + +/** + * Implementation of a [HabitList] that is backed by SQLite. + */ +class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory) : HabitList() { + private val repository: Repository + private val list: MemoryHabitList = MemoryHabitList() + private var loaded = false + private fun loadRecords() { + if (loaded) return + loaded = true + list.removeAll() + val records = repository.findAll("order by position") + var shouldRebuildOrder = false + for ((expectedPosition, rec) in records.withIndex()) { + if (rec.position != expectedPosition) shouldRebuildOrder = true + val h = modelFactory.buildHabit() + rec.copyTo(h) + (h.originalEntries as SQLiteEntryList).habitId = h.id + list.add(h) + } + if (shouldRebuildOrder) rebuildOrder() + } + + @Synchronized + override fun add(habit: Habit) { + loadRecords() + habit.position = size() + val record = HabitRecord() + record.copyFrom(habit) + repository.save(record) + habit.id = record.id + (habit.originalEntries as SQLiteEntryList).habitId = record.id + list.add(habit) + observable.notifyListeners() + } + + @Synchronized + override fun getById(id: Long): Habit? { + loadRecords() + return list.getById(id) + } + + @Synchronized + override fun getByUUID(uuid: String?): Habit? { + loadRecords() + return list.getByUUID(uuid) + } + + @Synchronized + override fun getByPosition(position: Int): Habit { + loadRecords() + return list.getByPosition(position) + } + + @Synchronized + override fun getFiltered(matcher: HabitMatcher?): HabitList { + loadRecords() + return list.getFiltered(matcher) + } + + @set:Synchronized + override var primaryOrder: Order + get() = list.primaryOrder + set(order) { + list.primaryOrder = order + observable.notifyListeners() + } + + @set:Synchronized + override var secondaryOrder: Order + get() = list.secondaryOrder + set(order) { + list.secondaryOrder = order + observable.notifyListeners() + } + + @Synchronized + override fun indexOf(h: Habit): Int { + loadRecords() + return list.indexOf(h) + } + + @Synchronized + override fun iterator(): Iterator { + loadRecords() + return list.iterator() + } + + @Synchronized + private fun rebuildOrder() { + val records = repository.findAll("order by position") + repository.executeAsTransaction { + for ((pos, r) in records.withIndex()) { + if (r.position != pos) { + r.position = pos + repository.save(r) + } + } + } + } + + @Synchronized + override fun remove(h: Habit) { + loadRecords() + reorder(h, list.getByPosition(size() - 1)) + list.remove(h) + val record = repository.find( + h.id!! + ) ?: throw RuntimeException("habit not in database") + repository.executeAsTransaction { + h.originalEntries.clear() + repository.remove(record) + } + observable.notifyListeners() + } + + @Synchronized + override fun removeAll() { + list.removeAll() + repository.execSQL("delete from habits") + repository.execSQL("delete from repetitions") + observable.notifyListeners() + } + + @Synchronized + override fun reorder(from: Habit, to: Habit) { + loadRecords() + list.reorder(from, to) + val fromRecord = repository.find( + from.id!! + ) + val toRecord = repository.find( + to.id!! + ) + if (fromRecord == null) throw RuntimeException("habit not in database") + if (toRecord == null) throw RuntimeException("habit not in database") + if (toRecord.position!! < fromRecord.position!!) { + repository.execSQL( + "update habits set position = position + 1 " + + "where position >= ? and position < ?", + toRecord.position!!, + fromRecord.position!! + ) + } else { + repository.execSQL( + "update habits set position = position - 1 " + + "where position > ? and position <= ?", + fromRecord.position!!, + toRecord.position!! + ) + } + fromRecord.position = toRecord.position + repository.save(fromRecord) + observable.notifyListeners() + } + + @Synchronized + override fun repair() { + loadRecords() + rebuildOrder() + observable.notifyListeners() + } + + @Synchronized + override fun size(): Int { + loadRecords() + return list.size() + } + + @Synchronized + override fun update(habits: List?) { + loadRecords() + list.update(habits) + for (h in habits!!) { + val record = repository.find( + h!!.id!! + ) ?: continue + record.copyFrom(h) + repository.save(record) + } + observable.notifyListeners() + } + + override fun resort() { + list.resort() + observable.notifyListeners() + } + + @Synchronized + fun reload() { + loaded = false + } + + init { + repository = modelFactory.buildHabitListRepository() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt similarity index 51% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt index 7355e6ce7..757c4a8f9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt @@ -16,40 +16,42 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.core.models.sqlite.records -package org.isoron.uhabits.core.models.sqlite.records; - -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.database.Column +import org.isoron.uhabits.core.database.Table +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.models.Timestamp /** - * The SQLite database record corresponding to a {@link Entry}. + * The SQLite database record corresponding to a [Entry]. */ @Table(name = "Repetitions") -public class EntryRecord -{ - public HabitRecord habit; - - @Column(name = "habit") - public Long habitId; +class EntryRecord { + var habit: HabitRecord? = null - @Column - public Long timestamp; + @field:Column(name = "habit") + var habitId: Long? = null - @Column - public Integer value; + @field:Column + var timestamp: Long? = null - @Column - public Long id; + @field:Column + var value: Int? = null - public void copyFrom(Entry entry) - { - timestamp = entry.getTimestamp().getUnixTime(); - value = entry.getValue(); + @field:Column + var id: Long? = null + fun copyFrom(entry: Entry) { + timestamp = entry.timestamp.unixTime + value = entry.value } - public Entry toEntry() - { - return new Entry(new Timestamp(timestamp), value); + fun toEntry(): Entry { + return Entry( + Timestamp( + timestamp!! + ), + value!! + ) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java deleted file mode 100644 index 8ddaeb183..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.models.sqlite.records; - -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; - -import java.util.Objects; - -/** - * The SQLite database record corresponding to a {@link Habit}. - */ -@Table(name = "habits") -public class HabitRecord -{ - @Column - public String description; - - @Column - public String question; - - @Column - public String name; - - @Column(name = "freq_num") - public Integer freqNum; - - @Column(name = "freq_den") - public Integer freqDen; - - @Column - public Integer color; - - @Column - public Integer position; - - @Column(name = "reminder_hour") - public Integer reminderHour; - - @Column(name = "reminder_min") - public Integer reminderMin; - - @Column(name = "reminder_days") - public Integer reminderDays; - - @Column - public Integer highlight; - - @Column - public Integer archived; - - @Column - public Integer type; - - @Column(name = "target_value") - public Double targetValue; - - @Column(name = "target_type") - public Integer targetType; - - @Column - public String unit; - - @Column - public Long id; - - @Column - public String uuid; - - public void copyFrom(Habit model) - { - this.id = model.getId(); - this.name = model.getName(); - this.description = model.getDescription(); - this.highlight = 0; - this.color = model.getColor().getPaletteIndex(); - this.archived = model.isArchived() ? 1 : 0; - this.type = model.getType(); - this.targetType = model.getTargetType(); - this.targetValue = model.getTargetValue(); - this.unit = model.getUnit(); - this.position = model.getPosition(); - this.question = model.getQuestion(); - this.uuid = model.getUuid(); - - Frequency freq = model.getFrequency(); - this.freqNum = freq.getNumerator(); - this.freqDen = freq.getDenominator(); - this.reminderDays = 0; - this.reminderMin = null; - this.reminderHour = null; - - if (model.hasReminder()) - { - Reminder reminder = model.getReminder(); - this.reminderHour = Objects.requireNonNull(reminder).getHour(); - this.reminderMin = reminder.getMinute(); - this.reminderDays = reminder.getDays().toInteger(); - } - } - - public void copyTo(Habit habit) - { - habit.setId(this.id); - habit.setName(this.name); - habit.setDescription(this.description); - habit.setQuestion(this.question); - habit.setFrequency(new Frequency(this.freqNum, this.freqDen)); - habit.setColor(new PaletteColor(this.color)); - habit.setArchived(this.archived != 0); - habit.setType(this.type); - habit.setTargetType(this.targetType); - habit.setTargetValue(this.targetValue); - habit.setUnit(this.unit); - habit.setPosition(this.position); - habit.setUuid(this.uuid); - - if (reminderHour != null && reminderMin != null) - { - habit.setReminder(new Reminder(reminderHour, reminderMin, - new WeekdayList(reminderDays))); - } - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt new file mode 100644 index 000000000..942a407a4 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.models.sqlite.records + +import org.isoron.uhabits.core.database.Column +import org.isoron.uhabits.core.database.Table +import org.isoron.uhabits.core.models.Frequency +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.WeekdayList +import java.util.Objects + +/** + * The SQLite database record corresponding to a [Habit]. + */ +@Table(name = "habits") +class HabitRecord { + @field:Column + var description: String? = null + + @field:Column + var question: String? = null + + @field:Column + var name: String? = null + + @field:Column(name = "freq_num") + var freqNum: Int? = null + + @field:Column(name = "freq_den") + var freqDen: Int? = null + + @field:Column + var color: Int? = null + + @field:Column + var position: Int? = null + + @field:Column(name = "reminder_hour") + var reminderHour: Int? = null + + @field:Column(name = "reminder_min") + var reminderMin: Int? = null + + @field:Column(name = "reminder_days") + var reminderDays: Int? = null + + @field:Column + var highlight: Int? = null + + @field:Column + var archived: Int? = null + + @field:Column + var type: Int? = null + + @field:Column(name = "target_value") + var targetValue: Double? = null + + @field:Column(name = "target_type") + var targetType: Int? = null + + @field:Column + var unit: String? = null + + @field:Column + var id: Long? = null + + @field:Column + var uuid: String? = null + fun copyFrom(model: Habit?) { + id = model!!.id + name = model.name + description = model.description + highlight = 0 + color = model.color.paletteIndex + archived = if (model.isArchived) 1 else 0 + type = model.type + targetType = model.targetType + targetValue = model.targetValue + unit = model.unit + position = model.position + question = model.question + uuid = model.uuid + val (numerator, denominator) = model.frequency + freqNum = numerator + freqDen = denominator + reminderDays = 0 + reminderMin = null + reminderHour = null + if (model.hasReminder()) { + val reminder = model.reminder + reminderHour = Objects.requireNonNull(reminder)!!.hour + reminderMin = reminder!!.minute + reminderDays = reminder.days.toInteger() + } + } + + fun copyTo(habit: Habit) { + habit.id = id + habit.name = name!! + habit.description = description!! + habit.question = question!! + habit.frequency = Frequency(freqNum!!, freqDen!!) + habit.color = PaletteColor(color!!) + habit.isArchived = archived != 0 + habit.type = type!! + habit.targetType = targetType!! + habit.targetValue = targetValue!! + habit.unit = unit!! + habit.position = position!! + habit.uuid = uuid + if (reminderHour != null && reminderMin != null) { + habit.reminder = Reminder( + reminderHour!!, + reminderMin!!, + WeekdayList(reminderDays!!) + ) + } + } +} From def9ff974606a0bba9f43265e428da36c4a7ecac Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 23:42:28 +0100 Subject: [PATCH 17/35] Convert activities.common.views (androidTest) --- .../common/views/FrequencyChartTest.java | 84 -------------- .../common/views/FrequencyChartTest.kt | 79 +++++++++++++ .../activities/common/views/RingViewTest.java | 73 ------------ .../activities/common/views/RingViewTest.kt | 65 +++++++++++ .../common/views/ScoreChartTest.java | 109 ------------------ .../activities/common/views/ScoreChartTest.kt | 108 +++++++++++++++++ .../common/views/StreakChartTest.java | 75 ------------ .../common/views/StreakChartTest.kt | 67 +++++++++++ 8 files changed, 319 insertions(+), 341 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.java deleted file mode 100644 index 519b1e5cb..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class FrequencyChartTest extends BaseViewTest -{ - public static final String BASE_PATH = "common/FrequencyChart/"; - - private FrequencyChart view; - - @Override - @Before - public void setUp() - { - super.setUp(); - - fixtures.purgeHabits(habitList); - Habit habit = fixtures.createLongHabit(); - - view = new FrequencyChart(targetContext); - view.setFrequency(habit.getOriginalEntries().computeWeekdayFrequency( - habit.isNumerical() - )); - view.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor())); - measureView(view, dpToPixels(300), dpToPixels(100)); - } - - @Test - public void testRender() throws Throwable - { - assertRenders(view, BASE_PATH + "render.png"); - } - - @Test - public void testRender_withDataOffset() throws Throwable - { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, BASE_PATH + "renderDataOffset.png"); - } - - @Test - public void testRender_withDifferentSize() throws Throwable - { - measureView(view, dpToPixels(200), dpToPixels(200)); - assertRenders(view, BASE_PATH + "renderDifferentSize.png"); - } - - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsBackgroundTransparent(true); - assertRenders(view, BASE_PATH + "renderTransparent.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt new file mode 100644 index 000000000..119d390e4 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.utils.toFixedAndroidColor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class FrequencyChartTest : BaseViewTest() { + private var view: FrequencyChart? = null + @Before + override fun setUp() { + super.setUp() + fixtures.purgeHabits(habitList) + val habit = fixtures.createLongHabit() + view = FrequencyChart(targetContext) + view!!.setFrequency( + habit.originalEntries.computeWeekdayFrequency( + habit.isNumerical + ) + ) + view!!.setColor(habit.color.toFixedAndroidColor()) + measureView(view, dpToPixels(300), dpToPixels(100)) + } + + @Test + @Throws(Throwable::class) + fun testRender() { + assertRenders(view, BASE_PATH + "render.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withDataOffset() { + view!!.onScroll(null, null, -dpToPixels(150), 0f) + view!!.invalidate() + assertRenders(view, BASE_PATH + "renderDataOffset.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withDifferentSize() { + measureView(view, dpToPixels(200), dpToPixels(200)) + assertRenders(view, BASE_PATH + "renderDifferentSize.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withTransparentBackground() { + view!!.setIsBackgroundTransparent(true) + assertRenders(view, BASE_PATH + "renderTransparent.png") + } + + companion object { + const val BASE_PATH = "common/FrequencyChart/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.java deleted file mode 100644 index 4f9528c28..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.graphics.*; -import androidx.test.filters.*; -import androidx.test.runner.*; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; -import org.junit.runner.*; - -import java.io.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class RingViewTest extends BaseViewTest -{ - private static final String BASE_PATH = "common/RingView/"; - - private RingView view; - - @Override - @Before - public void setUp() - { - super.setUp(); - - view = new RingView(targetContext); - view.setPercentage(0.6f); - view.setText("60%"); - view.setColor(PaletteUtils.getAndroidTestColor(0)); - view.setBackgroundColor(Color.WHITE); - view.setThickness(dpToPixels(3)); - } - - @Test - public void testRender_base() throws IOException - { - measureView(view, dpToPixels(100), dpToPixels(100)); - assertRenders(view, BASE_PATH + "render.png"); - } - - @Test - public void testRender_withDifferentParams() throws IOException - { - view.setPercentage(0.25f); - view.setColor(PaletteUtils.getAndroidTestColor(5)); - - measureView(view, dpToPixels(200), dpToPixels(200)); - assertRenders(view, BASE_PATH + "renderDifferentParams.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt new file mode 100644 index 000000000..cd6d1944c --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +@MediumTest +class RingViewTest : BaseViewTest() { + private var view: RingView? = null + @Before + override fun setUp() { + super.setUp() + view = RingView(targetContext) + view!!.setPercentage(0.6f) + view!!.setText("60%") + view!!.setColor(getAndroidTestColor(0)) + view!!.setBackgroundColor(Color.WHITE) + view!!.setThickness(dpToPixels(3)) + } + + @Test + @Throws(IOException::class) + fun testRender_base() { + measureView(view, dpToPixels(100), dpToPixels(100)) + assertRenders(view, BASE_PATH + "render.png") + } + + @Test + @Throws(IOException::class) + fun testRender_withDifferentParams() { + view!!.setPercentage(0.25f) + view!!.setColor(getAndroidTestColor(5)) + measureView(view, dpToPixels(200), dpToPixels(200)) + assertRenders(view, BASE_PATH + "renderDifferentParams.png") + } + + companion object { + private const val BASE_PATH = "common/RingView/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java deleted file mode 100644 index f33dd1590..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.ui.screens.habits.show.views.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class ScoreChartTest extends BaseViewTest -{ - private static final String BASE_PATH = "common/ScoreChart/"; - - private Habit habit; - - private ScoreChart view; - - @Override - @Before - public void setUp() - { - super.setUp(); - - fixtures.purgeHabits(habitList); - habit = fixtures.createLongHabit(); - ScoreCardState state = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 0); - - view = new ScoreChart(targetContext); - view.setScores(state.getScores()); - view.setColor(PaletteUtilsKt.toFixedAndroidColor(state.getColor())); - view.setBucketSize(state.getBucketSize()); - measureView(view, dpToPixels(300), dpToPixels(200)); - } - - @Test - public void testRender() throws Throwable - { - assertRenders(view, BASE_PATH + "render.png"); - } - - @Test - public void testRender_withDataOffset() throws Throwable - { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, BASE_PATH + "renderDataOffset.png"); - } - - @Test - public void testRender_withDifferentSize() throws Throwable - { - measureView(view, dpToPixels(200), dpToPixels(200)); - assertRenders(view, BASE_PATH + "renderDifferentSize.png"); - } - - @Test - public void testRender_withMonthlyBucket() throws Throwable - { - ScoreCardState model = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 2); - view.setScores(model.getScores()); - view.setBucketSize(model.getBucketSize()); - view.invalidate(); - - assertRenders(view, BASE_PATH + "renderMonthly.png"); - } - - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsTransparencyEnabled(true); - assertRenders(view, BASE_PATH + "renderTransparent.png"); - } - - @Test - public void testRender_withYearlyBucket() throws Throwable - { - ScoreCardState model = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 4); - view.setScores(model.getScores()); - view.setBucketSize(model.getBucketSize()); - view.invalidate(); - - assertRenders(view, BASE_PATH + "renderYearly.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt new file mode 100644 index 000000000..d3a9a7d94 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter.Companion.buildState +import org.isoron.uhabits.utils.toFixedAndroidColor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class ScoreChartTest : BaseViewTest() { + private lateinit var habit: Habit + private var view: ScoreChart? = null + @Before + override fun setUp() { + super.setUp() + fixtures.purgeHabits(habitList) + habit = fixtures.createLongHabit() + val (scores, bucketSize, _, color) = buildState(habit, prefs.firstWeekdayInt, 0) + view = ScoreChart(targetContext) + view!!.setScores(scores.toMutableList()) + view!!.setColor(color.toFixedAndroidColor()) + view!!.setBucketSize(bucketSize) + measureView(view, dpToPixels(300), dpToPixels(200)) + } + + @Test + @Throws(Throwable::class) + fun testRender() { + assertRenders(view, BASE_PATH + "render.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withDataOffset() { + view!!.onScroll(null, null, -dpToPixels(150), 0f) + view!!.invalidate() + assertRenders(view, BASE_PATH + "renderDataOffset.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withDifferentSize() { + measureView(view, dpToPixels(200), dpToPixels(200)) + assertRenders(view, BASE_PATH + "renderDifferentSize.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withMonthlyBucket() { + val (scores, bucketSize) = buildState( + habit!!, + prefs.firstWeekdayInt, + 2 + ) + view!!.setScores(scores.toMutableList()) + view!!.setBucketSize(bucketSize) + view!!.invalidate() + assertRenders(view, BASE_PATH + "renderMonthly.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withTransparentBackground() { + view!!.setIsTransparencyEnabled(true) + assertRenders(view, BASE_PATH + "renderTransparent.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withYearlyBucket() { + val (scores, bucketSize) = buildState( + habit!!, + prefs.firstWeekdayInt, + 4 + ) + view!!.setScores(scores.toMutableList()) + view!!.setBucketSize(bucketSize) + view!!.invalidate() + assertRenders(view, BASE_PATH + "renderYearly.png") + } + + companion object { + private const val BASE_PATH = "common/ScoreChart/" + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.java deleted file mode 100644 index 886a85a01..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import androidx.test.filters.*; -import androidx.test.runner.*; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; -import org.junit.runner.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class StreakChartTest extends BaseViewTest -{ - private static final String BASE_PATH = "common/StreakChart/"; - - private StreakChart view; - - @Override - @Before - public void setUp() - { - super.setUp(); - - fixtures.purgeHabits(habitList); - Habit habit = fixtures.createLongHabit(); - - view = new StreakChart(targetContext); - view.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor())); - view.setStreaks(habit.getStreaks().getBest(5)); - measureView(view, dpToPixels(300), dpToPixels(100)); - } - - @Test - public void testRender() throws Throwable - { - assertRenders(view, BASE_PATH + "render.png"); - } - - @Test - public void testRender_withSmallSize() throws Throwable - { - measureView(view, dpToPixels(100), dpToPixels(100)); - assertRenders(view, BASE_PATH + "renderSmallSize.png"); - } - - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsBackgroundTransparent(true); - assertRenders(view, BASE_PATH + "renderTransparent.png"); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt new file mode 100644 index 000000000..9182dfcb4 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseViewTest +import org.isoron.uhabits.utils.toFixedAndroidColor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class StreakChartTest : BaseViewTest() { + private var view: StreakChart? = null + @Before + override fun setUp() { + super.setUp() + fixtures.purgeHabits(habitList) + val (color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, streaks) = fixtures.createLongHabit() + view = StreakChart(targetContext) + view!!.setColor(color.toFixedAndroidColor()) + view!!.setStreaks(streaks.getBest(5)) + measureView(view, dpToPixels(300), dpToPixels(100)) + } + + @Test + @Throws(Throwable::class) + fun testRender() { + assertRenders(view, BASE_PATH + "render.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withSmallSize() { + measureView(view, dpToPixels(100), dpToPixels(100)) + assertRenders(view, BASE_PATH + "renderSmallSize.png") + } + + @Test + @Throws(Throwable::class) + fun testRender_withTransparentBackground() { + view!!.setIsBackgroundTransparent(true) + assertRenders(view, BASE_PATH + "renderTransparent.png") + } + + companion object { + private const val BASE_PATH = "common/StreakChart/" + } +} From ec42fda3369cf4e3d9e752352bdc65f501908302 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 00:01:56 +0100 Subject: [PATCH 18/35] Convert AndroidDatabaseTest --- .../uhabits/database/AndroidDatabaseTest.java | 59 ------------------- .../uhabits/database/AndroidDatabaseTest.kt | 49 +++++++++++++++ 2 files changed, 49 insertions(+), 59 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java deleted file mode 100644 index fe09fc951..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.database; - -import android.database.sqlite.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.database.*; -import org.junit.*; - -import java.util.*; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.core.IsEqual.*; - - -public class AndroidDatabaseTest extends BaseAndroidTest -{ - private AndroidDatabase db; - - @Override - public void setUp() - { - super.setUp(); - db = new AndroidDatabase(SQLiteDatabase.create(null), null); - db.execute("create table test(color int, name string)"); - } - - @Test - public void testInsert() throws Exception - { - HashMap map = new HashMap<>(); - map.put("name", "asd"); - map.put("color", null); - db.insert("test", map); - - Cursor c = db.query("select * from test"); - c.moveToNext(); - assertNull(c.getInt(0)); - assertThat(c.getString(1), equalTo("asd")); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt new file mode 100644 index 000000000..11661a09a --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.database + +import android.database.sqlite.SQLiteDatabase +import org.hamcrest.MatcherAssert +import org.hamcrest.core.IsEqual +import org.isoron.uhabits.BaseAndroidTest +import org.isoron.uhabits.core.database.Cursor +import org.junit.Test +import java.util.HashMap + +class AndroidDatabaseTest : BaseAndroidTest() { + private var db: AndroidDatabase? = null + override fun setUp() { + super.setUp() + db = AndroidDatabase(SQLiteDatabase.create(null), null) + db!!.execute("create table test(color int, name string)") + } + + @Test + @Throws(Exception::class) + fun testInsert() { + val map = HashMap() + map["name"] = "asd" + map["color"] = null + db!!.insert("test", map) + val c: Cursor = db!!.query("select * from test") + c.moveToNext() + assertNull(c.getInt(0)) + MatcherAssert.assertThat(c.getString(1), IsEqual.equalTo("asd")) + } +} From 7f0c4626b03bcc436cfd0202ef94d1a635c480fe Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 00:00:02 +0100 Subject: [PATCH 19/35] Convert HabitFixtures (androidtest) --- .../org/isoron/uhabits/HabitFixtures.java | 164 ------------------ .../java/org/isoron/uhabits/HabitFixtures.kt | 140 +++++++++++++++ 2 files changed, 140 insertions(+), 164 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java deleted file mode 100644 index fb3c901d5..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.DateUtils; - -import static org.isoron.uhabits.core.models.Entry.*; - -public class HabitFixtures -{ - public boolean LONG_HABIT_ENTRIES[] = { - true, false, false, true, true, true, false, false, true, true - }; - - public int LONG_NUMERICAL_HABIT_ENTRIES[] = { - 200000, 0, 150000, 137000, 0, 0, 500000, 30000, 100000, 0, 300000, - 100000, 0, 100000 - }; - - private ModelFactory modelFactory; - - private final HabitList habitList; - - public HabitFixtures(ModelFactory modelFactory, HabitList habitList) - { - this.modelFactory = modelFactory; - this.habitList = habitList; - } - - public Habit createEmptyHabit() - { - return createEmptyHabit(null); - } - - public Habit createEmptyHabit(Long id) - { - Habit habit = modelFactory.buildHabit(); - habit.setName("Meditate"); - habit.setQuestion("Did you meditate this morning?"); - habit.setDescription("This is a test description"); - habit.setColor(new PaletteColor(5)); - habit.setFrequency(Frequency.DAILY); - habit.setId(id); - habitList.add(habit); - return habit; - } - - public Habit createLongHabit() - { - Habit habit = createEmptyHabit(); - habit.setFrequency(new Frequency(3, 7)); - habit.setColor(new PaletteColor(7)); - - Timestamp today = DateUtils.getToday(); - int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, - 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, - 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; - - for (int mark : marks) - habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL)); - - habit.recompute(); - return habit; - } - - public Habit createVeryLongHabit() - { - Habit habit = createEmptyHabit(); - habit.setFrequency(new Frequency(1, 2)); - habit.setColor(new PaletteColor(11)); - - Timestamp today = DateUtils.getToday(); - int marks[] = {0, 3, 5, 6, 7, 10, 13, 14, 15, 18, 21, 22, 23, 24, 27, 28, 30, 31, 34, 37, - 39, 42, 43, 46, 47, 48, 51, 52, 54, 55, 57, 59, 62, 65, 68, 71, 73, 76, 79, - 80, 81, 83, 85, 86, 89, 90, 91, 94, 96, 98, 100, 103, 104, 106, 109, 111, - 112, 113, 115, 117, 120, 123, 126, 129, 132, 134, 136, 139, 141, 142, 145, - 148, 149, 151, 152, 154, 156, 157, 159, 161, 162, 163, 164, 166, 168, 170, - 172, 173, 174, 175, 176, 178, 180, 181, 184, 185, 188, 189, 190, 191, 194, - 195, 197, 198, 199, 200, 202, 205, 208, 211, 213, 215, 216, 218, 220, 222, - 223, 225, 227, 228, 230, 231, 232, 234, 235, 238, 241, 242, 244, 247, 250, - 251, 253, 254, 257, 260, 261, 263, 264, 266, 269, 272, 273, 276, 279, 281, - 284, 285, 288, 291, 292, 294, 296, 297, 299, 300, 301, 303, 306, 307, 308, - 309, 310, 313, 316, 319, 322, 324, 326, 329, 330, 332, 334, 335, 337, 338, - 341, 344, 345, 346, 347, 350, 352, 355, 358, 360, 361, 362, 363, 365, 368, - 371, 373, 374, 376, 379, 380, 382, 384, 385, 387, 389, 390, 392, 393, 395, - 396, 399, 401, 404, 407, 410, 411, 413, 414, 416, 417, 419, 420, 423, 424, - 427, 429, 431, 433, 436, 439, 440, 442, 445, 447, 450, 453, 454, 456, 459, - 460, 461, 464, 466, 468, 470, 473, 474, 475, 477, 479, 481, 482, 483, 486, - 489, 491, 493, 495, 497, 498, 500, 503, 504, 507, 510, 511, 512, 515, 518, - 519, 521, 522, 525, 528, 531, 532, 534, 537, 539, 541, 543, 544, 547, 550, - 551, 554, 556, 557, 560, 561, 564, 567, 568, 569, 570, 572, 575, 576, 579, - 582, 583, 584, 586, 589}; - - for (int mark : marks) - habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL)); - - habit.recompute(); - return habit; - } - - public Habit createLongNumericalHabit() - { - Habit habit = modelFactory.buildHabit(); - habit.setName("Read"); - habit.setQuestion("How many pages did you walk today?"); - habit.setType(Habit.NUMBER_HABIT); - habit.setTargetType(Habit.AT_LEAST); - habit.setTargetValue(200.0); - habit.setUnit("pages"); - habitList.add(habit); - - Timestamp timestamp = DateUtils.getToday(); - for (int value : LONG_NUMERICAL_HABIT_ENTRIES) - { - habit.getOriginalEntries().add(new Entry(timestamp, value)); - timestamp = timestamp.minus(1); - } - - habit.recompute(); - return habit; - } - - public Habit createShortHabit() - { - Habit habit = modelFactory.buildHabit(); - habit.setName("Wake up early"); - habit.setQuestion("Did you wake up before 6am?"); - habit.setFrequency(new Frequency(2, 3)); - habitList.add(habit); - - Timestamp timestamp = DateUtils.getToday(); - for (boolean c : LONG_HABIT_ENTRIES) - { - if (c) habit.getOriginalEntries().add(new Entry(timestamp, YES_MANUAL)); - timestamp = timestamp.minus(1); - } - - habit.recompute(); - return habit; - } - - public synchronized void purgeHabits(HabitList habitList) - { - habitList.removeAll(); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt new file mode 100644 index 000000000..93485c2e8 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits + +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL +import org.isoron.uhabits.core.models.Frequency +import org.isoron.uhabits.core.models.Frequency.Companion.DAILY +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.Habit.Companion.AT_LEAST +import org.isoron.uhabits.core.models.Habit.Companion.NUMBER_HABIT +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.ModelFactory +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday + +class HabitFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList) { + var LONG_HABIT_ENTRIES = booleanArrayOf( + true, false, false, true, true, true, false, false, true, true + ) + var LONG_NUMERICAL_HABIT_ENTRIES = intArrayOf( + 200000, 0, 150000, 137000, 0, 0, 500000, 30000, 100000, 0, 300000, + 100000, 0, 100000 + ) + + @JvmOverloads + fun createEmptyHabit(id: Long? = null): Habit { + val habit = modelFactory.buildHabit() + habit.name = "Meditate" + habit.question = "Did you meditate this morning?" + habit.description = "This is a test description" + habit.color = PaletteColor(5) + habit.frequency = DAILY + habit.id = id + habitList.add(habit) + return habit + } + + fun createLongHabit(): Habit { + val habit = createEmptyHabit() + habit.frequency = Frequency(3, 7) + habit.color = PaletteColor(7) + val today: Timestamp = getToday() + val marks = intArrayOf( + 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, + 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, + 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120 + ) + for (mark in marks) habit.originalEntries.add(Entry(today.minus(mark), YES_MANUAL)) + habit.recompute() + return habit + } + + fun createVeryLongHabit(): Habit { + val habit = createEmptyHabit() + habit.frequency = Frequency(1, 2) + habit.color = PaletteColor(11) + val today: Timestamp = getToday() + val marks = intArrayOf( + 0, 3, 5, 6, 7, 10, 13, 14, 15, 18, 21, 22, 23, 24, 27, 28, 30, 31, 34, 37, + 39, 42, 43, 46, 47, 48, 51, 52, 54, 55, 57, 59, 62, 65, 68, 71, 73, 76, 79, + 80, 81, 83, 85, 86, 89, 90, 91, 94, 96, 98, 100, 103, 104, 106, 109, 111, + 112, 113, 115, 117, 120, 123, 126, 129, 132, 134, 136, 139, 141, 142, 145, + 148, 149, 151, 152, 154, 156, 157, 159, 161, 162, 163, 164, 166, 168, 170, + 172, 173, 174, 175, 176, 178, 180, 181, 184, 185, 188, 189, 190, 191, 194, + 195, 197, 198, 199, 200, 202, 205, 208, 211, 213, 215, 216, 218, 220, 222, + 223, 225, 227, 228, 230, 231, 232, 234, 235, 238, 241, 242, 244, 247, 250, + 251, 253, 254, 257, 260, 261, 263, 264, 266, 269, 272, 273, 276, 279, 281, + 284, 285, 288, 291, 292, 294, 296, 297, 299, 300, 301, 303, 306, 307, 308, + 309, 310, 313, 316, 319, 322, 324, 326, 329, 330, 332, 334, 335, 337, 338, + 341, 344, 345, 346, 347, 350, 352, 355, 358, 360, 361, 362, 363, 365, 368, + 371, 373, 374, 376, 379, 380, 382, 384, 385, 387, 389, 390, 392, 393, 395, + 396, 399, 401, 404, 407, 410, 411, 413, 414, 416, 417, 419, 420, 423, 424, + 427, 429, 431, 433, 436, 439, 440, 442, 445, 447, 450, 453, 454, 456, 459, + 460, 461, 464, 466, 468, 470, 473, 474, 475, 477, 479, 481, 482, 483, 486, + 489, 491, 493, 495, 497, 498, 500, 503, 504, 507, 510, 511, 512, 515, 518, + 519, 521, 522, 525, 528, 531, 532, 534, 537, 539, 541, 543, 544, 547, 550, + 551, 554, 556, 557, 560, 561, 564, 567, 568, 569, 570, 572, 575, 576, 579, + 582, 583, 584, 586, 589 + ) + for (mark in marks) habit.originalEntries.add(Entry(today.minus(mark), YES_MANUAL)) + habit.recompute() + return habit + } + + fun createLongNumericalHabit(): Habit { + val habit = modelFactory.buildHabit() + habit.name = "Read" + habit.question = "How many pages did you walk today?" + habit.type = NUMBER_HABIT + habit.targetType = AT_LEAST + habit.targetValue = 200.0 + habit.unit = "pages" + habitList.add(habit) + var timestamp: Timestamp = getToday() + for (value in LONG_NUMERICAL_HABIT_ENTRIES) { + habit.originalEntries.add(Entry(timestamp, value)) + timestamp = timestamp.minus(1) + } + habit.recompute() + return habit + } + + fun createShortHabit(): Habit { + val habit = modelFactory.buildHabit() + habit.name = "Wake up early" + habit.question = "Did you wake up before 6am?" + habit.frequency = Frequency(2, 3) + habitList.add(habit) + var timestamp: Timestamp = getToday() + for (c in LONG_HABIT_ENTRIES) { + if (c) habit.originalEntries.add(Entry(timestamp, YES_MANUAL)) + timestamp = timestamp.minus(1) + } + habit.recompute() + return habit + } + + @Synchronized + fun purgeHabits(habitList: HabitList) { + habitList.removeAll() + } +} From df755d30ee92c3449283d533a044884c5fb79c97 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 00:14:40 +0100 Subject: [PATCH 20/35] Convert HabitsApplicationTest --- ...tionTest.java => HabitsApplicationTest.kt} | 39 ++++++++----------- ...java => HabitsApplicationTestComponent.kt} | 38 +++++------------- .../org/isoron/uhabits/SingleThreadModule.kt | 17 ++++++++ .../uhabits/intents/IntentSchedulerTest.kt | 4 +- 4 files changed, 45 insertions(+), 53 deletions(-) rename uhabits-android/src/androidTest/java/org/isoron/uhabits/{HabitsApplicationTest.java => HabitsApplicationTest.kt} (56%) rename uhabits-android/src/androidTest/java/org/isoron/uhabits/{HabitsApplicationTestComponent.java => HabitsApplicationTestComponent.kt} (56%) create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/SingleThreadModule.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt similarity index 56% rename from uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java rename to uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt index d681ba0e0..f6996903f 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt @@ -16,32 +16,25 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits -package org.isoron.uhabits; +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException -import androidx.test.filters.*; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.*; -import org.junit.runner.*; - -import java.io.*; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.*; - -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidJUnit4::class) @MediumTest -public class HabitsApplicationTest extends BaseAndroidTest -{ +class HabitsApplicationTest : BaseAndroidTest() { @Test - public void test_getLogcat() throws IOException - { - String msg = "LOGCAT TEST"; - new RuntimeException(msg).printStackTrace(); - - String log = new AndroidBugReporter(targetContext).getLogcat(); - assertThat(log, containsString(msg)); + @Throws(IOException::class) + fun test_getLogcat() { + val msg = "LOGCAT TEST" + RuntimeException(msg).printStackTrace() + val log = AndroidBugReporter(targetContext).getLogcat() + MatcherAssert.assertThat(log, CoreMatchers.containsString(msg)) } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.kt similarity index 56% rename from uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java rename to uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.kt index 8693399c7..751baaae8 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.kt @@ -16,35 +16,17 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits -package org.isoron.uhabits; - -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.inject.*; -import org.isoron.uhabits.intents.*; - -import dagger.*; +import dagger.Component +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.inject.AppContextModule +import org.isoron.uhabits.inject.HabitsApplicationComponent +import org.isoron.uhabits.inject.HabitsModule +import org.isoron.uhabits.intents.IntentScheduler @AppScope -@Component(modules = { - AppContextModule.class, - HabitsModule.class, - SingleThreadModule.class, -}) -public interface HabitsApplicationTestComponent - extends HabitsApplicationComponent -{ - IntentScheduler getIntentScheduler(); -} - -@dagger.Module -class SingleThreadModule -{ - @Provides - @AppScope - static TaskRunner provideTaskRunner() - { - return new SingleThreadTaskRunner(); - } +@Component(modules = [AppContextModule::class, HabitsModule::class, SingleThreadModule::class]) +interface HabitsApplicationTestComponent : HabitsApplicationComponent { + val intentScheduler: IntentScheduler? } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/SingleThreadModule.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/SingleThreadModule.kt new file mode 100644 index 000000000..b07aaee63 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/SingleThreadModule.kt @@ -0,0 +1,17 @@ +package org.isoron.uhabits + +import dagger.Module +import dagger.Provides +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner +import org.isoron.uhabits.core.tasks.TaskRunner + +@Module +internal object SingleThreadModule { + @JvmStatic + @Provides + @AppScope + fun provideTaskRunner(): TaskRunner { + return SingleThreadTaskRunner() + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt index 36a1926bd..eaabde242 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt @@ -97,7 +97,7 @@ class IntentSchedulerTest : BaseAndroidTest() { val habit = habitList.getByPosition(0) val scheduler = appComponent.intentScheduler - assertThat(scheduler.scheduleShowReminder(reminderTime, habit, 0), equalTo(OK)) + assertThat(scheduler!!.scheduleShowReminder(reminderTime, habit, 0), equalTo(OK)) setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44) assertNull(ReminderReceiver.lastReceivedIntent) @@ -116,7 +116,7 @@ class IntentSchedulerTest : BaseAndroidTest() { val updateTime = 1591155900000 // 2020-06-02 22:45:00 (America/Chicago) val scheduler = appComponent.intentScheduler - assertThat(scheduler.scheduleWidgetUpdate(updateTime), equalTo(OK)) + assertThat(scheduler!!.scheduleWidgetUpdate(updateTime), equalTo(OK)) setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44) assertNull(WidgetReceiver.lastReceivedIntent) From 21fa636e0c8c48b0a40e228e2efcc53f9bd559d8 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 11:38:52 +0100 Subject: [PATCH 21/35] Convert BaseAndroidTest --- .../org/isoron/uhabits/BaseAndroidTest.java | 289 ------------------ .../org/isoron/uhabits/BaseAndroidTest.kt | 219 +++++++++++++ 2 files changed, 219 insertions(+), 289 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java deleted file mode 100644 index 280c40c55..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits; - -import android.appwidget.*; -import android.content.*; -import android.content.res.*; -import android.os.*; -import android.util.*; - -import androidx.annotation.*; -import androidx.test.filters.*; -import androidx.test.platform.app.*; -import androidx.test.uiautomator.*; - -import junit.framework.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.inject.*; -import org.isoron.uhabits.utils.*; -import org.junit.*; - -import java.io.*; -import java.time.*; -import java.util.*; -import java.util.concurrent.*; - -import static androidx.test.platform.app.InstrumentationRegistry.*; -import static androidx.test.uiautomator.UiDevice.*; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.*; - -@MediumTest -public class BaseAndroidTest extends TestCase -{ - // 8:00am, January 25th, 2015 (UTC) - public static final long FIXED_LOCAL_TIME = 1422172800000L; - - protected Context testContext; - - protected Context targetContext; - - protected Preferences prefs; - - protected HabitList habitList; - - protected TaskRunner taskRunner; - - protected HabitFixtures fixtures; - - protected CountDownLatch latch; - - protected HabitsApplicationTestComponent appComponent; - - protected ModelFactory modelFactory; - - protected HabitsActivityTestComponent component; - - private boolean isDone = false; - - private UiDevice device; - - @Override - @Before - public void setUp() - { - if (Looper.myLooper() == null) Looper.prepare(); - device = getInstance(getInstrumentation()); - - targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - testContext = InstrumentationRegistry.getInstrumentation().getContext(); - - DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME); - DateUtils.setStartDayOffset(0, 0); - setResolution(2.0f); - setTheme(R.style.AppBaseTheme); - setLocale("en", "US"); - - latch = new CountDownLatch(1); - - Context context = targetContext.getApplicationContext(); - File dbFile = DatabaseUtils.getDatabaseFile(context); - appComponent = DaggerHabitsApplicationTestComponent - .builder() - .appContextModule(new AppContextModule(context)) - .habitsModule(new HabitsModule(dbFile)) - .build(); - - HabitsApplication.Companion.setComponent(appComponent); - prefs = appComponent.getPreferences(); - habitList = appComponent.getHabitList(); - taskRunner = appComponent.getTaskRunner(); - modelFactory = appComponent.getModelFactory(); - - prefs.clear(); - - fixtures = new HabitFixtures(modelFactory, habitList); - fixtures.purgeHabits(appComponent.getHabitList()); - Habit habit = fixtures.createEmptyHabit(); - - component = DaggerHabitsActivityTestComponent - .builder() - .activityContextModule(new ActivityContextModule(targetContext)) - .habitsApplicationComponent(appComponent) - .build(); - } - - protected void assertWidgetProviderIsInstalled(Class componentClass) - { - ComponentName provider = - new ComponentName(targetContext, componentClass); - AppWidgetManager manager = AppWidgetManager.getInstance(targetContext); - - List installedProviders = new LinkedList<>(); - for (AppWidgetProviderInfo info : manager.getInstalledProviders()) - installedProviders.add(info.provider); - - assertThat(installedProviders, hasItems(provider)); - } - - protected void awaitLatch() throws InterruptedException - { - assertTrue(latch.await(1, TimeUnit.SECONDS)); - } - - protected void setLocale(@NonNull String language, @NonNull String country) - { - Locale locale = new Locale(language, country); - Locale.setDefault(locale); - Resources res = targetContext.getResources(); - Configuration config = res.getConfiguration(); - config.setLocale(locale); - } - - protected void setResolution(float r) - { - DisplayMetrics dm = targetContext.getResources().getDisplayMetrics(); - dm.density = r; - dm.scaledDensity = r; - InterfaceUtils.setFixedResolution(r); - } - - protected void runConcurrently(Runnable... runnableList) throws Exception - { - isDone = false; - ExecutorService executor = Executors.newFixedThreadPool(100); - List futures = new LinkedList<>(); - for (Runnable r : runnableList) - futures.add(executor.submit(() -> - { - while (!isDone) r.run(); - return null; - })); - - Thread.sleep(3000); - isDone = true; - executor.shutdown(); - for(Future f : futures) f.get(); - while (!executor.isTerminated()) Thread.sleep(50); - } - - protected void setTheme(@StyleRes int themeId) - { - targetContext.setTheme(themeId); - StyledResources.setFixedTheme(themeId); - } - - protected void sleep(int time) - { - try - { - Thread.sleep(time); - } - catch (InterruptedException e) - { - fail(); - } - } - - public long timestamp(int year, int month, int day) - { - GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); - cal.set(year, month, day); - return cal.getTimeInMillis(); - } - - protected void startTracing() - { - File dir = new AndroidDirFinder(targetContext).getFilesDir("Profile"); - assertNotNull(dir); - String tracePath = dir.getAbsolutePath() + "/performance.trace"; - Log.d("PerformanceTest", String.format("Saving trace file to %s", tracePath)); - Debug.startMethodTracingSampling(tracePath, 0, 1000); - } - - protected void stopTracing() - { - Debug.stopMethodTracing(); - } - - protected Timestamp day(int offset) - { - return DateUtils.getToday().minus(offset); - } - - - public void setSystemTime(String tz, - int year, - int javaMonth, - int day, - int hourOfDay, - int minute) throws Exception - { - GregorianCalendar cal = new GregorianCalendar(); - cal.set(Calendar.SECOND, 0); - cal.set(year, javaMonth, day, hourOfDay, minute); - cal.setTimeZone(TimeZone.getTimeZone(tz)); - setSystemTime(cal); - } - - private void setSystemTime(GregorianCalendar cal) throws Exception - { - ZoneId tz = cal.getTimeZone().toZoneId(); - - // Set time zone (temporary) - String command = String.format("service call alarm 3 s16 %s", tz); - device.executeShellCommand(command); - - // Set time zone (permanent) - command = String.format("setprop persist.sys.timezone %s", tz); - device.executeShellCommand(command); - - // Set time - String date = String.format("%02d%02d%02d%02d%02d.%02d", - cal.get(Calendar.MONTH) + 1, - cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.HOUR_OF_DAY), - cal.get(Calendar.MINUTE), - cal.get(Calendar.YEAR), - cal.get(Calendar.SECOND)); - - // Set time (method 1) - // Run twice to override daylight saving time - device.executeShellCommand("date " + date); - device.executeShellCommand("date " + date); - - // Set time (method 2) - // Run in addition to the method above because one of these mail fail, depending - // on the Android API version. - command = String.format("date -u @%d", cal.getTimeInMillis() / 1000); - device.executeShellCommand(command); - - // Wait for system events to settle - Thread.sleep(1000); - } - - private GregorianCalendar savedCalendar = null; - - public void saveSystemTime() - { - savedCalendar = new GregorianCalendar(); - } - - public void restoreSystemTime() throws Exception - { - if (savedCalendar == null) throw new NullPointerException(); - setSystemTime(savedCalendar); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt new file mode 100644 index 000000000..61e9e121e --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.os.Looper +import androidx.annotation.StyleRes +import androidx.test.filters.MediumTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import junit.framework.TestCase +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.ModelFactory +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime +import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset +import org.isoron.uhabits.inject.ActivityContextModule +import org.isoron.uhabits.inject.AppContextModule +import org.isoron.uhabits.inject.HabitsModule +import org.isoron.uhabits.utils.DatabaseUtils.getDatabaseFile +import org.isoron.uhabits.utils.InterfaceUtils.setFixedResolution +import org.isoron.uhabits.utils.StyledResources.Companion.setFixedTheme +import org.isoron.uhabits.widgets.BaseWidgetProvider +import org.junit.Before +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.LinkedList +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.CountDownLatch + +@MediumTest +abstract class BaseAndroidTest : TestCase() { + @JvmField + protected var testContext: Context = InstrumentationRegistry.getInstrumentation().context + + @JvmField + protected var targetContext: Context = + InstrumentationRegistry.getInstrumentation().targetContext + protected lateinit var prefs: Preferences + + protected lateinit var habitList: HabitList + protected lateinit var taskRunner: TaskRunner + protected lateinit var fixtures: HabitFixtures + protected lateinit var latch: CountDownLatch + protected lateinit var appComponent: HabitsApplicationTestComponent + protected lateinit var modelFactory: ModelFactory + protected lateinit var component: HabitsActivityTestComponent + private lateinit var device: UiDevice + + @Before + public override fun setUp() { + if (Looper.myLooper() == null) Looper.prepare() + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + setFixedLocalTime(FIXED_LOCAL_TIME) + setStartDayOffset(0, 0) + setResolution(2.0f) + setTheme(R.style.AppBaseTheme) + setLocale("en", "US") + latch = CountDownLatch(1) + val context = targetContext.applicationContext + val dbFile = getDatabaseFile(context) + appComponent = DaggerHabitsApplicationTestComponent + .builder() + .appContextModule(AppContextModule(context)) + .habitsModule(HabitsModule(dbFile)) + .build() + HabitsApplication.component = appComponent + prefs = appComponent.preferences + habitList = appComponent.habitList + taskRunner = appComponent.taskRunner + modelFactory = appComponent.modelFactory + prefs.clear() + fixtures = HabitFixtures(modelFactory, habitList) + fixtures.purgeHabits(appComponent.habitList) + fixtures.createEmptyHabit() + component = DaggerHabitsActivityTestComponent + .builder() + .activityContextModule(ActivityContextModule(targetContext)) + .habitsApplicationComponent(appComponent) + .build() + } + + protected fun assertWidgetProviderIsInstalled(componentClass: Class?) { + val provider = ComponentName(targetContext, componentClass!!) + val manager = AppWidgetManager.getInstance(targetContext) + val installedProviders: MutableList = LinkedList() + for (info in manager.installedProviders) installedProviders.add(info.provider) + MatcherAssert.assertThat>( + installedProviders, + CoreMatchers.hasItems(provider) + ) + } + + protected fun setLocale(language: String, country: String) { + val locale = Locale(language, country) + Locale.setDefault(locale) + val res = targetContext.resources + val config = res.configuration + config.setLocale(locale) + } + + protected fun setResolution(r: Float) { + val dm = targetContext.resources.displayMetrics + dm.density = r + dm.scaledDensity = r + setFixedResolution(r) + } + + protected fun setTheme(@StyleRes themeId: Int) { + targetContext.setTheme(themeId) + setFixedTheme(themeId) + } + + protected fun sleep(time: Int) { + try { + Thread.sleep(time.toLong()) + } catch (e: InterruptedException) { + fail() + } + } + + protected fun day(offset: Int): Timestamp { + return getToday().minus(offset) + } + + @Throws(Exception::class) + fun setSystemTime( + tz: String?, + year: Int, + javaMonth: Int, + day: Int, + hourOfDay: Int, + minute: Int + ) { + val cal = GregorianCalendar() + cal[Calendar.SECOND] = 0 + cal[year, javaMonth, day, hourOfDay] = minute + cal.timeZone = TimeZone.getTimeZone(tz) + setSystemTime(cal) + } + + @Throws(Exception::class) + private fun setSystemTime(cal: GregorianCalendar) { + val tz = cal.timeZone.toZoneId() + + // Set time zone (temporary) + var command = String.format("service call alarm 3 s16 %s", tz) + device.executeShellCommand(command) + + // Set time zone (permanent) + command = String.format("setprop persist.sys.timezone %s", tz) + device.executeShellCommand(command) + + // Set time + val date = String.format( + "%02d%02d%02d%02d%02d.%02d", + cal[Calendar.MONTH] + 1, + cal[Calendar.DAY_OF_MONTH], + cal[Calendar.HOUR_OF_DAY], + cal[Calendar.MINUTE], + cal[Calendar.YEAR], + cal[Calendar.SECOND] + ) + + // Set time (method 1) + // Run twice to override daylight saving time + device.executeShellCommand("date $date") + device.executeShellCommand("date $date") + + // Set time (method 2) + // Run in addition to the method above because one of these mail fail, depending + // on the Android API version. + command = String.format("date -u @%d", cal.timeInMillis / 1000) + device.executeShellCommand(command) + + // Wait for system events to settle + Thread.sleep(1000) + } + + private var savedCalendar: GregorianCalendar? = null + fun saveSystemTime() { + savedCalendar = GregorianCalendar() + } + + @Throws(Exception::class) + fun restoreSystemTime() { + if (savedCalendar == null) throw NullPointerException() + setSystemTime(savedCalendar!!) + } + + companion object { + // 8:00am, January 25th, 2015 (UTC) + const val FIXED_LOCAL_TIME = 1422172800000L + } +} From 7644f52dfdc36272c117b3cb7fd85ac88cc7df8d Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 17:50:18 +0100 Subject: [PATCH 22/35] Don't open ThingRecord --- .../org/isoron/uhabits/core/database/ThingRecord.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt index 47c4eaaba..ac3a252db 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt @@ -5,18 +5,18 @@ import org.apache.commons.lang3.builder.HashCodeBuilder import org.apache.commons.lang3.builder.ToStringBuilder @Table(name = "tests") -open class ThingRecord { +class ThingRecord { @field:Column - open var id: Long? = null + var id: Long? = null @field:Column - open var name: String? = null + var name: String? = null @field:Column(name = "color_number") - open var color: Int? = null + var color: Int? = null @field:Column - open var score: Double? = null + var score: Double? = null override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false From 09794ccb68522d355a170b42d392ec939a505931 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 19:47:19 +0100 Subject: [PATCH 23/35] Move ThingRecord back into RepositoryTest --- .../uhabits/core/database/RepositoryTest.kt | 44 ++++++++++++++++++ .../uhabits/core/database/ThingRecord.kt | 46 ------------------- 2 files changed, 44 insertions(+), 46 deletions(-) delete mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt index 1d13b98a0..96ffa6cbd 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt @@ -18,6 +18,9 @@ */ package org.isoron.uhabits.core.database +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.apache.commons.lang3.builder.ToStringBuilder import org.hamcrest.MatcherAssert import org.hamcrest.core.IsEqual import org.isoron.uhabits.core.BaseUnitTest @@ -153,4 +156,45 @@ class RepositoryTest : BaseUnitTest() { repository!!.remove(rec1) // should have no effect Assert.assertNull(repository!!.find(id)) } + + @Table(name = "tests") + class ThingRecord { + @field:Column + var id: Long? = null + + @field:Column + var name: String? = null + + @field:Column(name = "color_number") + var color: Int? = null + + @field:Column + var score: Double? = null + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val record = other as ThingRecord + return EqualsBuilder() + .append(id, record.id) + .append(name, record.name) + .append(color, record.color) + .isEquals + } + + override fun hashCode(): Int { + return HashCodeBuilder(17, 37) + .append(id) + .append(name) + .append(color) + .toHashCode() + } + + override fun toString(): String { + return ToStringBuilder(this) + .append("id", id) + .append("name", name) + .append("color", color) + .toString() + } + } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt deleted file mode 100644 index ac3a252db..000000000 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/ThingRecord.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.isoron.uhabits.core.database - -import org.apache.commons.lang3.builder.EqualsBuilder -import org.apache.commons.lang3.builder.HashCodeBuilder -import org.apache.commons.lang3.builder.ToStringBuilder - -@Table(name = "tests") -class ThingRecord { - @field:Column - var id: Long? = null - - @field:Column - var name: String? = null - - @field:Column(name = "color_number") - var color: Int? = null - - @field:Column - var score: Double? = null - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val record = other as ThingRecord - return EqualsBuilder() - .append(id, record.id) - .append(name, record.name) - .append(color, record.color) - .isEquals - } - - override fun hashCode(): Int { - return HashCodeBuilder(17, 37) - .append(id) - .append(name) - .append(color) - .toHashCode() - } - - override fun toString(): String { - return ToStringBuilder(this) - .append("id", id) - .append("name", name) - .append("color", color) - .toString() - } -} From 18db571507dff46d144482ae00975aacd27a45a6 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 20:04:58 +0100 Subject: [PATCH 24/35] Properly check that a setter is never used --- .../core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt index 27f54c72f..2485c8ba5 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehaviorTest.kt @@ -19,6 +19,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list import com.nhaarman.mockitokotlin2.KArgumentCaptor +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.clearInvocations import com.nhaarman.mockitokotlin2.mock @@ -125,7 +126,7 @@ class ListHabitsMenuBehaviorTest : BaseUnitTest() { whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_STATUS_ASC) behavior.onSortByStatus() verify(adapter).primaryOrder = orderCaptor.capture() - verify(adapter, never()).secondaryOrder + verify(adapter, never()).secondaryOrder = any() assertThat(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_DESC)) } From 6992b5186e87d7d8ea3b7b970c4a4fcfcc52013e Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 18:17:58 +0100 Subject: [PATCH 25/35] Clean up code after conversions --- .../org/isoron/uhabits/BaseAndroidTest.kt | 4 +- .../java/org/isoron/uhabits/HabitFixtures.kt | 28 ++-- .../common/views/FrequencyChartTest.kt | 20 ++- .../activities/common/views/RingViewTest.kt | 20 +-- .../activities/common/views/ScoreChartTest.kt | 33 ++--- .../common/views/StreakChartTest.kt | 13 +- .../habits/list/views/HeaderViewTest.kt | 18 +-- .../habits/list/views/HintViewTest.kt | 2 - .../uhabits/database/AndroidDatabaseTest.kt | 8 +- .../uhabits/widgets/TargetWidgetTest.kt | 9 +- .../widgets/views/CheckmarkWidgetViewTest.kt | 19 ++- .../common/views/BundleSavedState.kt | 2 +- .../activities/common/views/FrequencyChart.kt | 17 ++- .../activities/common/views/RingView.kt | 6 +- .../activities/common/views/ScoreChart.kt | 16 ++- .../common/views/ScrollableChart.kt | 41 +++--- .../activities/common/views/StreakChart.kt | 23 ++-- .../activities/common/views/TargetChart.kt | 10 +- .../habits/list/ListHabitsScreen.kt | 28 ++-- .../habits/list/views/HabitCardListAdapter.kt | 12 +- .../habits/list/views/HabitCardView.kt | 3 - .../habits/show/views/OverviewCardView.kt | 2 - .../isoron/uhabits/intents/IntentScheduler.kt | 2 +- .../notifications/AndroidNotificationTray.kt | 8 +- .../SnoozeDelayPickerActivity.kt | 1 + .../org/isoron/uhabits/widgets/BaseWidget.kt | 10 +- .../widgets/views/CheckmarkWidgetView.kt | 2 - .../isoron/uhabits/core/models/HabitList.kt | 16 +-- .../isoron/uhabits/core/models/Timestamp.kt | 2 +- .../core/models/memory/MemoryHabitList.kt | 14 +- .../core/models/sqlite/SQLiteHabitList.kt | 14 +- .../core/models/sqlite/records/EntryRecord.kt | 7 +- .../core/models/sqlite/records/HabitRecord.kt | 5 +- .../core/reminders/ReminderScheduler.kt | 2 +- .../uhabits/core/ui/NotificationTray.kt | 20 +-- .../screens/habits/list/HabitCardListCache.kt | 16 +-- .../screens/habits/list/ListHabitsBehavior.kt | 16 +-- .../habits/list/ListHabitsMenuBehavior.kt | 5 +- .../list/ListHabitsSelectionMenuBehavior.kt | 18 +-- .../uhabits/core/database/RepositoryTest.kt | 129 ++++++++---------- .../uhabits/core/models/EntryListTest.kt | 2 +- .../models/sqlite/records/HabitRecordTest.kt | 4 +- .../uhabits/sync/app/BaseApplicationTest.kt | 4 +- .../uhabits/sync/app/StorageModuleTest.kt | 2 +- 44 files changed, 284 insertions(+), 349 deletions(-) diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt index 61e9e121e..76722b92e 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt @@ -201,7 +201,7 @@ abstract class BaseAndroidTest : TestCase() { Thread.sleep(1000) } - private var savedCalendar: GregorianCalendar? = null + private lateinit var savedCalendar: GregorianCalendar fun saveSystemTime() { savedCalendar = GregorianCalendar() } @@ -209,7 +209,7 @@ abstract class BaseAndroidTest : TestCase() { @Throws(Exception::class) fun restoreSystemTime() { if (savedCalendar == null) throw NullPointerException() - setSystemTime(savedCalendar!!) + setSystemTime(savedCalendar) } companion object { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt index 93485c2e8..3cc5f14fc 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt @@ -40,15 +40,13 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis 100000, 0, 100000 ) - @JvmOverloads - fun createEmptyHabit(id: Long? = null): Habit { + fun createEmptyHabit(): Habit { val habit = modelFactory.buildHabit() habit.name = "Meditate" habit.question = "Did you meditate this morning?" habit.description = "This is a test description" habit.color = PaletteColor(5) habit.frequency = DAILY - habit.id = id habitList.add(habit) return habit } @@ -101,13 +99,14 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis } fun createLongNumericalHabit(): Habit { - val habit = modelFactory.buildHabit() - habit.name = "Read" - habit.question = "How many pages did you walk today?" - habit.type = NUMBER_HABIT - habit.targetType = AT_LEAST - habit.targetValue = 200.0 - habit.unit = "pages" + val habit = modelFactory.buildHabit().apply { + name = "Read" + question = "How many pages did you walk today?" + type = NUMBER_HABIT + targetType = AT_LEAST + targetValue = 200.0 + unit = "pages" + } habitList.add(habit) var timestamp: Timestamp = getToday() for (value in LONG_NUMERICAL_HABIT_ENTRIES) { @@ -119,10 +118,11 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis } fun createShortHabit(): Habit { - val habit = modelFactory.buildHabit() - habit.name = "Wake up early" - habit.question = "Did you wake up before 6am?" - habit.frequency = Frequency(2, 3) + val habit = modelFactory.buildHabit().apply { + name = "Wake up early" + question = "Did you wake up before 6am?" + frequency = Frequency(2, 3) + } habitList.add(habit) var timestamp: Timestamp = getToday() for (c in LONG_HABIT_ENTRIES) { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt index 119d390e4..0c4225f99 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt @@ -29,19 +29,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @MediumTest class FrequencyChartTest : BaseViewTest() { - private var view: FrequencyChart? = null + private lateinit var view: FrequencyChart + @Before override fun setUp() { super.setUp() fixtures.purgeHabits(habitList) val habit = fixtures.createLongHabit() - view = FrequencyChart(targetContext) - view!!.setFrequency( - habit.originalEntries.computeWeekdayFrequency( - habit.isNumerical - ) - ) - view!!.setColor(habit.color.toFixedAndroidColor()) + view = FrequencyChart(targetContext).apply { + setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical)) + setColor(habit.color.toFixedAndroidColor()) + } measureView(view, dpToPixels(300), dpToPixels(100)) } @@ -54,8 +52,8 @@ class FrequencyChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withDataOffset() { - view!!.onScroll(null, null, -dpToPixels(150), 0f) - view!!.invalidate() + view.onScroll(null, null, -dpToPixels(150), 0f) + view.invalidate() assertRenders(view, BASE_PATH + "renderDataOffset.png") } @@ -69,7 +67,7 @@ class FrequencyChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withTransparentBackground() { - view!!.setIsBackgroundTransparent(true) + view.setIsBackgroundTransparent(true) assertRenders(view, BASE_PATH + "renderTransparent.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt index cd6d1944c..9617296b9 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.kt @@ -31,16 +31,18 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @MediumTest class RingViewTest : BaseViewTest() { - private var view: RingView? = null + private lateinit var view: RingView + @Before override fun setUp() { super.setUp() - view = RingView(targetContext) - view!!.setPercentage(0.6f) - view!!.setText("60%") - view!!.setColor(getAndroidTestColor(0)) - view!!.setBackgroundColor(Color.WHITE) - view!!.setThickness(dpToPixels(3)) + view = RingView(targetContext).apply { + setPercentage(0.6f) + setText("60%") + setColor(getAndroidTestColor(0)) + setBackgroundColor(Color.WHITE) + setThickness(dpToPixels(3)) + } } @Test @@ -53,8 +55,8 @@ class RingViewTest : BaseViewTest() { @Test @Throws(IOException::class) fun testRender_withDifferentParams() { - view!!.setPercentage(0.25f) - view!!.setColor(getAndroidTestColor(5)) + view.setPercentage(0.25f) + view.setColor(getAndroidTestColor(5)) measureView(view, dpToPixels(200), dpToPixels(200)) assertRenders(view, BASE_PATH + "renderDifferentParams.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt index d3a9a7d94..c4d1cbc87 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt @@ -32,17 +32,18 @@ import org.junit.runner.RunWith @MediumTest class ScoreChartTest : BaseViewTest() { private lateinit var habit: Habit - private var view: ScoreChart? = null + private lateinit var view: ScoreChart @Before override fun setUp() { super.setUp() fixtures.purgeHabits(habitList) habit = fixtures.createLongHabit() val (scores, bucketSize, _, color) = buildState(habit, prefs.firstWeekdayInt, 0) - view = ScoreChart(targetContext) - view!!.setScores(scores.toMutableList()) - view!!.setColor(color.toFixedAndroidColor()) - view!!.setBucketSize(bucketSize) + view = ScoreChart(targetContext).apply { + setScores(scores.toMutableList()) + setColor(color.toFixedAndroidColor()) + setBucketSize(bucketSize) + } measureView(view, dpToPixels(300), dpToPixels(200)) } @@ -55,8 +56,8 @@ class ScoreChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withDataOffset() { - view!!.onScroll(null, null, -dpToPixels(150), 0f) - view!!.invalidate() + view.onScroll(null, null, -dpToPixels(150), 0f) + view.invalidate() assertRenders(view, BASE_PATH + "renderDataOffset.png") } @@ -71,20 +72,20 @@ class ScoreChartTest : BaseViewTest() { @Throws(Throwable::class) fun testRender_withMonthlyBucket() { val (scores, bucketSize) = buildState( - habit!!, + habit, prefs.firstWeekdayInt, 2 ) - view!!.setScores(scores.toMutableList()) - view!!.setBucketSize(bucketSize) - view!!.invalidate() + view.setScores(scores.toMutableList()) + view.setBucketSize(bucketSize) + view.invalidate() assertRenders(view, BASE_PATH + "renderMonthly.png") } @Test @Throws(Throwable::class) fun testRender_withTransparentBackground() { - view!!.setIsTransparencyEnabled(true) + view.setIsTransparencyEnabled(true) assertRenders(view, BASE_PATH + "renderTransparent.png") } @@ -92,13 +93,13 @@ class ScoreChartTest : BaseViewTest() { @Throws(Throwable::class) fun testRender_withYearlyBucket() { val (scores, bucketSize) = buildState( - habit!!, + habit, prefs.firstWeekdayInt, 4 ) - view!!.setScores(scores.toMutableList()) - view!!.setBucketSize(bucketSize) - view!!.invalidate() + view.setScores(scores.toMutableList()) + view.setBucketSize(bucketSize) + view.invalidate() assertRenders(view, BASE_PATH + "renderYearly.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt index 9182dfcb4..58ed5e7d3 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.kt @@ -29,15 +29,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @MediumTest class StreakChartTest : BaseViewTest() { - private var view: StreakChart? = null + private lateinit var view: StreakChart @Before override fun setUp() { super.setUp() fixtures.purgeHabits(habitList) - val (color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, streaks) = fixtures.createLongHabit() - view = StreakChart(targetContext) - view!!.setColor(color.toFixedAndroidColor()) - view!!.setStreaks(streaks.getBest(5)) + val habit = fixtures.createLongHabit() + view = StreakChart(targetContext).apply { + setColor(habit.color.toFixedAndroidColor()) + setStreaks(habit.streaks.getBest(5)) + } measureView(view, dpToPixels(300), dpToPixels(100)) } @@ -57,7 +58,7 @@ class StreakChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withTransparentBackground() { - view!!.setIsBackgroundTransparent(true) + view.setIsBackgroundTransparent(true) assertRenders(view, BASE_PATH + "renderTransparent.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt index ccfebd949..6bf7e2a81 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HeaderViewTest.kt @@ -22,44 +22,44 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.whenever import org.isoron.uhabits.BaseViewTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito @RunWith(AndroidJUnit4::class) @MediumTest class HeaderViewTest : BaseViewTest() { - private var view: HeaderView? = null + private lateinit var view: HeaderView @Before override fun setUp() { super.setUp() prefs = mock() view = HeaderView(targetContext, prefs, mock()) - view!!.buttonCount = 5 + view.buttonCount = 5 measureView(view, dpToPixels(600), dpToPixels(48)) } @Test @Throws(Exception::class) fun testRender() { - Mockito.`when`(prefs.isCheckmarkSequenceReversed).thenReturn(false) + whenever(prefs.isCheckmarkSequenceReversed).thenReturn(false) assertRenders(view, PATH + "render.png") - Mockito.verify(prefs).isCheckmarkSequenceReversed - Mockito.verifyNoMoreInteractions(prefs) + verify(prefs).isCheckmarkSequenceReversed + verifyNoMoreInteractions(prefs) } @Test @Throws(Exception::class) fun testRender_reverse() { doReturn(true).whenever(prefs).isCheckmarkSequenceReversed - // Mockito.`when`(prefs.isCheckmarkSequenceReversed).thenReturn(true) assertRenders(view, PATH + "render_reverse.png") - Mockito.verify(prefs).isCheckmarkSequenceReversed - Mockito.verifyNoMoreInteractions(prefs) + verify(prefs).isCheckmarkSequenceReversed + verifyNoMoreInteractions(prefs) } companion object { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt index 6685acc9e..1ff4104bd 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt @@ -45,8 +45,6 @@ class HintViewTest : BaseViewTest() { val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." doReturn(true).whenever(list).shouldShow() doReturn(text).whenever(list).pop() - // Mockito.`when`(list.shouldShow()).thenReturn(true) - // Mockito.`when`(list.pop()).thenReturn(text) view.showNext() skipAnimation(view) } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt index 11661a09a..6fe668e3f 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt @@ -27,11 +27,11 @@ import org.junit.Test import java.util.HashMap class AndroidDatabaseTest : BaseAndroidTest() { - private var db: AndroidDatabase? = null + private lateinit var db: AndroidDatabase override fun setUp() { super.setUp() db = AndroidDatabase(SQLiteDatabase.create(null), null) - db!!.execute("create table test(color int, name string)") + db.execute("create table test(color int, name string)") } @Test @@ -40,8 +40,8 @@ class AndroidDatabaseTest : BaseAndroidTest() { val map = HashMap() map["name"] = "asd" map["color"] = null - db!!.insert("test", map) - val c: Cursor = db!!.query("select * from test") + db.insert("test", map) + val c: Cursor = db.query("select * from test") c.moveToNext() assertNull(c.getInt(0)) MatcherAssert.assertThat(c.getString(1), IsEqual.equalTo("asd")) diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt index fd49a0eb1..41477c0ae 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/TargetWidgetTest.kt @@ -38,10 +38,11 @@ class TargetWidgetTest : BaseViewTest() { super.setUp() setTheme(R.style.WidgetTheme) prefs.widgetOpacity = 255 - habit = fixtures.createLongNumericalHabit() - habit.color = PaletteColor(11) - habit.frequency = Frequency.WEEKLY - habit.recompute() + habit = fixtures.createLongNumericalHabit().apply { + color = PaletteColor(11) + frequency = Frequency.WEEKLY + recompute() + } val widget = TargetWidget(targetContext, 0, habit) view = convertToView(widget, 400, 400) } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt index 9cd494152..37538e758 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt @@ -32,25 +32,24 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @MediumTest class CheckmarkWidgetViewTest : BaseViewTest() { - private var view: CheckmarkWidgetView? = null + private lateinit var view: CheckmarkWidgetView @Before override fun setUp() { super.setUp() setTheme(R.style.WidgetTheme) val habit = fixtures.createShortHabit() - val name = habit.name val computedEntries = habit.computedEntries val scores = habit.scores val today = getTodayWithOffset() - view = CheckmarkWidgetView(targetContext) val score = scores[today].value - val percentage = score.toFloat() - view!!.activeColor = getAndroidTestColor(0) - view!!.entryState = computedEntries.get(today).value - view!!.entryValue = computedEntries.get(today).value - view!!.percentage = percentage - view!!.name = name - view!!.refresh() + view = CheckmarkWidgetView(targetContext).apply { + activeColor = getAndroidTestColor(0) + entryState = computedEntries.get(today).value + entryValue = computedEntries.get(today).value + percentage = score.toFloat() + name = habit.name + } + view.refresh() measureView(view, dpToPixels(100), dpToPixels(200)) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt index 3537259dd..5f8a09395 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt @@ -25,7 +25,7 @@ import android.os.Parcelable.ClassLoaderCreator import androidx.customview.view.AbsSavedState class BundleSavedState : AbsSavedState { - val bundle: Bundle? + @JvmField val bundle: Bundle? constructor(superState: Parcelable?, bundle: Bundle?) : super(superState!!) { this.bundle = bundle diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt index f0c85d036..73579a858 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt @@ -38,6 +38,9 @@ import java.util.GregorianCalendar import java.util.Locale import java.util.Random import kotlin.collections.HashMap +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt class FrequencyChart : ScrollableChart { private var pGrid: Paint? = null @@ -90,8 +93,8 @@ class FrequencyChart : ScrollableChart { private fun getMaxFreq(frequency: HashMap>): Int { var maxValue = 1 - for (values in frequency.values) for (value in values) maxValue = Math.max( - value!!, + for (values in frequency.values) for (value in values) maxValue = max( + value, maxValue ) return maxValue @@ -102,7 +105,7 @@ class FrequencyChart : ScrollableChart { initColors() } - protected fun initPaints() { + private fun initPaints() { pText = Paint() pText!!.isAntiAlias = true pGraph = Paint() @@ -155,7 +158,7 @@ class FrequencyChart : ScrollableChart { pGrid!!.strokeWidth = baseSize * 0.05f em = pText!!.fontSpacing columnWidth = baseSize.toFloat() - columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + columnWidth = max(columnWidth, maxMonthWidth * 1.2f) columnHeight = 8 * baseSize nColumns = (width / columnWidth).toInt() internalPaddingTop = 0 @@ -225,20 +228,20 @@ class FrequencyChart : ScrollableChart { // the real mark radius is scaled down by a factor depending on the maximal frequency val scale = 1.0f / maxFreq * value!! val radius = maxRadius * scale - val colorIndex = Math.min(colors.size - 1, Math.round((colors.size - 1) * scale)) + val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt()) pGraph!!.color = colors[colorIndex] canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph!!) } private val maxMonthWidth: Float - private get() { + get() { var maxMonthWidth = 0f val day: GregorianCalendar = getStartOfTodayCalendarWithOffset() for (i in 0..11) { day[Calendar.MONTH] = i val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) - maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + maxMonthWidth = max(maxMonthWidth, monthWidth) } return maxMonthWidth } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt index 67635bfaf..2da90bf26 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt @@ -41,6 +41,8 @@ import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.InterfaceUtils.spToPixels import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor import org.isoron.uhabits.utils.StyledResources +import kotlin.math.min +import kotlin.math.roundToLong class RingView : View { private var color: Int @@ -140,7 +142,7 @@ class RingView : View { } pRing!!.color = color rect!![0f, 0f, diameter.toFloat()] = diameter.toFloat() - val angle = 360 * Math.round(percentage / precision) * precision + val angle = 360 * (percentage / precision).roundToLong() * precision activeCanvas!!.drawArc(rect!!, -90f, angle, true, pRing!!) pRing!!.color = inactiveColor!! activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!) @@ -167,7 +169,7 @@ class RingView : View { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) - diameter = Math.min(height, width) + diameter = min(height, width) pRing!!.textSize = textSize em = pRing!!.measureText("M") setMeasuredDimension(diameter, diameter) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt index 9a0a5bb76..286f20216 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt @@ -42,6 +42,8 @@ import java.util.GregorianCalendar import java.util.LinkedList import java.util.Locale import java.util.Random +import kotlin.math.max +import kotlin.math.min class ScoreChart : ScrollableChart { private var pGrid: Paint? = null @@ -89,7 +91,7 @@ class ScoreChart : ScrollableChart { for (i in 1..99) { val step = 0.1 var current = previous + random.nextDouble() * step * 2 - step - current = Math.max(0.0, Math.min(1.0, current)) + current = max(0.0, min(1.0, current)) newScores.add(Score(timestamp.minus(i), current)) previous = current } @@ -178,14 +180,14 @@ class ScoreChart : ScrollableChart { if (height < 9) height = 200 val maxTextSize = getDimension(context, R.dimen.tinyTextSize) val textSize = height * 0.06f - pText!!.textSize = Math.min(textSize, maxTextSize) + pText!!.textSize = min(textSize, maxTextSize) em = pText!!.fontSpacing val footerHeight = (3 * em).toInt() internalPaddingTop = em.toInt() baseSize = (height - footerHeight - internalPaddingTop) / 8 columnWidth = baseSize.toFloat() - columnWidth = Math.max(columnWidth, maxDayWidth * 1.5f) - columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + columnWidth = max(columnWidth, maxDayWidth * 1.5f) + columnWidth = max(columnWidth, maxMonthWidth * 1.2f) nColumns = (width / columnWidth).toInt() columnWidth = width.toFloat() / nColumns setScrollerBucketSize(columnWidth.toInt()) @@ -193,7 +195,7 @@ class ScoreChart : ScrollableChart { val minStrokeWidth = dpToPixels(context, 1f) pGraph!!.textSize = baseSize * 0.5f pGraph!!.strokeWidth = baseSize * 0.1f - pGrid!!.strokeWidth = Math.min(minStrokeWidth, baseSize * 0.05f) + pGrid!!.strokeWidth = min(minStrokeWidth, baseSize * 0.05f) if (isTransparencyEnabled) initCache(width, height) } @@ -298,7 +300,7 @@ class ScoreChart : ScrollableChart { for (i in 0..27) { day[Calendar.DAY_OF_MONTH] = i val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) - maxDayWidth = Math.max(maxDayWidth, monthWidth) + maxDayWidth = max(maxDayWidth, monthWidth) } return maxDayWidth } @@ -310,7 +312,7 @@ class ScoreChart : ScrollableChart { for (i in 0..11) { day[Calendar.MONTH] = i val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) - maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + maxMonthWidth = max(maxMonthWidth, monthWidth) } return maxMonthWidth } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt index a1c86449a..113b37076 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt @@ -28,6 +28,9 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.widget.Scroller +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min abstract class ScrollableChart : View, GestureDetector.OnGestureListener, AnimatorUpdateListener { var dataOffset = 0 @@ -91,37 +94,37 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat super.onRestoreInstanceState(state) return } - val bss = state - val x = bss.bundle!!.getInt("x") - val y = bss.bundle.getInt("y") - direction = bss.bundle.getInt("direction") - dataOffset = bss.bundle.getInt("dataOffset") - maxDataOffset = bss.bundle.getInt("maxDataOffset") + val x = state.bundle!!.getInt("x") + val y = state.bundle.getInt("y") + direction = state.bundle.getInt("direction") + dataOffset = state.bundle.getInt("dataOffset") + maxDataOffset = state.bundle.getInt("maxDataOffset") scroller!!.startScroll(0, 0, x, y, 0) scroller!!.computeScrollOffset() - super.onRestoreInstanceState(bss.superState) + super.onRestoreInstanceState(state.superState) } public override fun onSaveInstanceState(): Parcelable? { val superState = super.onSaveInstanceState() - val bundle = Bundle() - bundle.putInt("x", scroller!!.currX) - bundle.putInt("y", scroller!!.currY) - bundle.putInt("dataOffset", dataOffset) - bundle.putInt("direction", direction) - bundle.putInt("maxDataOffset", maxDataOffset) + val bundle = Bundle().apply { + putInt("x", scroller!!.currX) + putInt("y", scroller!!.currY) + putInt("dataOffset", dataOffset) + putInt("direction", direction) + putInt("maxDataOffset", maxDataOffset) + } return BundleSavedState(superState, bundle) } override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean { var dx = dx if (scrollerBucketSize == 0) return false - if (Math.abs(dx) > Math.abs(dy)) { + if (abs(dx) > abs(dy)) { val parent = parent parent?.requestDisallowInterceptTouchEvent(true) } - dx = -direction * dx - dx = Math.min(dx, (maxX - scroller!!.currX).toFloat()) + dx *= -direction + dx = min(dx, (maxX - scroller!!.currX).toFloat()) scroller!!.startScroll( scroller!!.currX, scroller!!.currY, @@ -151,7 +154,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat override fun onLongPress(e: MotionEvent) {} fun setMaxDataOffset(maxDataOffset: Int) { this.maxDataOffset = maxDataOffset - dataOffset = Math.min(dataOffset, maxDataOffset) + dataOffset = min(dataOffset, maxDataOffset) scrollController!!.onDataOffsetChanged(dataOffset) postInvalidate() } @@ -181,8 +184,8 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat private fun updateDataOffset() { var newDataOffset = scroller!!.currX / scrollerBucketSize - newDataOffset = Math.max(0, newDataOffset) - newDataOffset = Math.min(maxDataOffset, newDataOffset) + newDataOffset = max(0, newDataOffset) + newDataOffset = min(maxDataOffset, newDataOffset) if (newDataOffset != dataOffset) { dataOffset = newDataOffset scrollController!!.onDataOffsetChanged(dataOffset) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt index 098813921..b821c1c9a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt @@ -37,6 +37,9 @@ import java.text.DateFormat import java.util.LinkedList import java.util.Random import java.util.TimeZone +import kotlin.math.floor +import kotlin.math.max +import kotlin.math.min class StreakChart : View { private var paint: Paint? = null @@ -72,7 +75,7 @@ class StreakChart : View { * @return max number of visible streaks */ val maxStreakCount: Int - get() = Math.floor((measuredHeight / baseSize).toDouble()).toInt() + get() = floor((measuredHeight / baseSize).toDouble()).toInt() fun populateWithRandomData() { var start: Timestamp = getToday() @@ -105,7 +108,7 @@ class StreakChart : View { override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if (streaks!!.size == 0) return + if (streaks!!.isEmpty()) return rect!![0f, 0f, internalWidth.toFloat()] = baseSize.toFloat() for (s in streaks!!) { drawRow(canvas, s, rect) @@ -137,7 +140,7 @@ class StreakChart : View { val minTextSize = getDimension(context, R.dimen.tinyTextSize) val maxTextSize = getDimension(context, R.dimen.regularTextSize) val textSize = baseSize * 0.5f - paint!!.textSize = Math.max(Math.min(textSize, maxTextSize), minTextSize) + paint!!.textSize = max(min(textSize, maxTextSize), minTextSize) em = paint!!.fontSpacing textMargin = 0.5f * em updateMaxMinLengths() @@ -149,8 +152,8 @@ class StreakChart : View { var availableWidth = internalWidth - 2 * maxLabelWidth if (shouldShowLabels) availableWidth -= 2 * textMargin var barWidth = percentage * availableWidth - val minBarWidth = paint!!.measureText(java.lang.Long.toString(streak.length.toLong())) + em - barWidth = Math.max(barWidth, minBarWidth) + val minBarWidth = paint!!.measureText(streak.length.toLong().toString()) + em + barWidth = max(barWidth, minBarWidth) val gap = (internalWidth - barWidth) / 2 val paddingTopBottom = baseSize * 0.05f paint!!.color = percentageToColor(percentage) @@ -168,7 +171,7 @@ class StreakChart : View { paint!!.color = percentageToTextColor(percentage) paint!!.textAlign = Paint.Align.CENTER canvas.drawText( - java.lang.Long.toString(streak.length.toLong()), + streak.length.toLong().toString(), rect.centerX(), yOffset, paint!! @@ -189,7 +192,7 @@ class StreakChart : View { initColors() streaks = emptyList() val newDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) - if (!isInEditMode) newDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")) + if (!isInEditMode) newDateFormat.timeZone = TimeZone.getTimeZone("GMT") dateFormat = newDateFormat rect = RectF() baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) @@ -232,11 +235,11 @@ class StreakChart : View { minLength = Long.MAX_VALUE shouldShowLabels = true for (s in streaks!!) { - maxLength = Math.max(maxLength, s.length.toLong()) - minLength = Math.min(minLength, s.length.toLong()) + maxLength = max(maxLength, s.length.toLong()) + minLength = min(minLength, s.length.toLong()) val lw1 = paint!!.measureText(dateFormat!!.format(s.start.toJavaDate())) val lw2 = paint!!.measureText(dateFormat!!.format(s.end.toJavaDate())) - maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)) + maxLabelWidth = max(maxLabelWidth, max(lw1, lw2)) } if (internalWidth - 2 * maxLabelWidth < internalWidth * 0.25f) { maxLabelWidth = 0f diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt index 296263f9e..c87640524 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt @@ -31,6 +31,8 @@ import org.isoron.uhabits.activities.habits.list.views.toShortString import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.StyledResources +import kotlin.math.max +import kotlin.math.min class TargetChart : View { private var paint: Paint? = null @@ -62,12 +64,12 @@ class TargetChart : View { override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if (labels.size == 0) return + if (labels.isEmpty()) return maxLabelSize = 0f for (label in labels) { paint!!.textSize = tinyTextSize val len = paint!!.measureText(label) - maxLabelSize = Math.max(maxLabelSize, len) + maxLabelSize = max(maxLabelSize, len) } val marginTop = (height - baseSize * labels.size) / 2.0f rect[0f, marginTop, width.toFloat()] = marginTop + baseSize @@ -86,7 +88,7 @@ class TargetChart : View { val params = layoutParams if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) { height = MeasureSpec.getSize(heightSpec) - if (labels.size > 0) baseSize = height / labels.size + if (labels.isNotEmpty()) baseSize = height / labels.size } heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) @@ -116,7 +118,7 @@ class TargetChart : View { rect.bottom - baseSize * 0.05f canvas.drawRoundRect(barRect, round, round, paint!!) var percentage = (values[row] / targets[row]).toFloat() - percentage = Math.min(1.0f, percentage) + percentage = min(1.0f, percentage) // Draw completed box var completedWidth = percentage * barRect.width() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index e2e265aaf..6168ed781 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -173,8 +173,8 @@ class ListHabitsScreen ConfirmDeleteDialog(activity, callback, quantity).show() } - override fun showEditHabitsScreen(habits: List) { - val intent = intentFactory.startEditActivity(activity, habits[0]) + override fun showEditHabitsScreen(selected: List) { + val intent = intentFactory.startEditActivity(activity, selected[0]) activity.startActivity(intent) } @@ -183,8 +183,8 @@ class ListHabitsScreen activity.startActivity(intent) } - override fun showHabitScreen(habit: Habit) { - val intent = intentFactory.startShowHabitActivity(activity, habit) + override fun showHabitScreen(h: Habit) { + val intent = intentFactory.startShowHabitActivity(activity, h) activity.startActivity(intent) } @@ -215,7 +215,7 @@ class ListHabitsScreen ) } - override fun showSendBugReportToDeveloperScreen(log: String?) { + override fun showSendBugReportToDeveloperScreen(log: String) { val to = R.string.bugReportTo val subject = R.string.bugReportSubject activity.showSendEmailScreen(to, subject, log) @@ -287,13 +287,17 @@ class ListHabitsScreen private fun onImportData(file: File, onFinished: () -> Unit) { taskRunner.execute( importTaskFactory.create(file) { result -> - if (result == ImportDataTask.SUCCESS) { - adapter.refresh() - activity.showMessage(activity.resources.getString(R.string.habits_imported)) - } else if (result == ImportDataTask.NOT_RECOGNIZED) { - activity.showMessage(activity.resources.getString(R.string.file_not_recognized)) - } else { - activity.showMessage(activity.resources.getString(R.string.could_not_import)) + when (result) { + ImportDataTask.SUCCESS -> { + adapter.refresh() + activity.showMessage(activity.resources.getString(R.string.habits_imported)) + } + ImportDataTask.NOT_RECOGNIZED -> { + activity.showMessage(activity.resources.getString(R.string.file_not_recognized)) + } + else -> { + activity.showMessage(activity.resources.getString(R.string.could_not_import)) + } } onFinished() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 8e3a7d96d..bdfaebd62 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -90,10 +90,6 @@ class HabitCardListAdapter @Inject constructor( return getItem(position)!!.id!! } - // override fun getSelected(): List { - // return LinkedList(selected) - // } - /** * Returns whether list of selected items is empty. * @@ -158,8 +154,8 @@ class HabitCardListAdapter @Inject constructor( observable.notifyListeners() } - override fun onItemMoved(fromPosition: Int, toPosition: Int) { - notifyItemMoved(fromPosition, toPosition) + override fun onItemMoved(oldPosition: Int, newPosition: Int) { + notifyItemMoved(oldPosition, newPosition) observable.notifyListeners() } @@ -209,8 +205,8 @@ class HabitCardListAdapter @Inject constructor( cache.refreshAllHabits() } - override fun setFilter(matcher: HabitMatcher?) { - if (matcher != null) cache.setFilter(matcher) + override fun setFilter(matcher: HabitMatcher) { + cache.setFilter(matcher) } /** diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index f54d15e04..26af7f3e9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -92,9 +92,7 @@ class HabitCardView( var score get() = scoreRing.getPercentage().toDouble() set(value) { - // scoreRing.percentage = value.toFloat() scoreRing.setPercentage(value.toFloat()) - // scoreRing.precision = 1.0f / 16 scoreRing.setPrecision(1.0f / 16) } @@ -228,7 +226,6 @@ class HabitCardView( } scoreRing.apply { setColor(c) - // color = c } checkmarkPanel.apply { color = c diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt index 1de75f34a..95614c903 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt @@ -49,9 +49,7 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff) binding.scoreLabel.setTextColor(androidColor) binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100) - // binding.scoreRing.color = androidColor binding.scoreRing.setColor(androidColor) - // binding.scoreRing.percentage = state.scoreToday binding.scoreRing.setPercentage(state.scoreToday) binding.title.setTextColor(androidColor) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt index 5d699944c..5a4c79f1d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt @@ -80,7 +80,7 @@ class IntentScheduler return schedule(updateTime, intent, RTC) } - override fun log(componentName: String?, msg: String?) { + override fun log(componentName: String, msg: String) { Log.d(componentName, msg) } 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 8f697d490..de1c23d24 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 @@ -53,14 +53,14 @@ class AndroidNotificationTray ) : NotificationTray.SystemTray { private var active = HashSet() - override fun log(msg: String?) { + override fun log(msg: String) { Log.d("AndroidNotificationTray", msg) } - override fun removeNotification(id: Int) { + override fun removeNotification(notificationId: Int) { val manager = NotificationManagerCompat.from(context) - manager.cancel(id) - active.remove(id) + manager.cancel(notificationId) + active.remove(notificationId) } override fun showNotification( diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt index eeb0730b9..ace88e61d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/SnoozeDelayPickerActivity.kt @@ -43,6 +43,7 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener { private var dialog: AlertDialog? = null override fun onCreate(bundle: Bundle?) { super.onCreate(bundle) + val intent = intent if (intent == null) finish() if (intent.data == null) finish() val app = applicationContext as HabitsApplication diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidget.kt index aef9a3a4c..451d1edfe 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/BaseWidget.kt @@ -34,7 +34,7 @@ import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.intents.PendingIntentFactory abstract class BaseWidget(val context: Context, val id: Int) { - protected val widgetPrefs: WidgetPreferences + private val widgetPrefs: WidgetPreferences protected val prefs: Preferences protected val pendingIntentFactory: PendingIntentFactory protected val commandRunner: CommandRunner @@ -120,9 +120,9 @@ abstract class BaseWidget(val context: Context, val id: Int) { return remoteViews } - private fun measureView(view: View, width: Int, height: Int) { - var width = width - var height = height + private fun measureView(view: View, w: Int, h: Int) { + var width = w + var height = h val inflater = LayoutInflater.from(context) val entireView = inflater.inflate(R.layout.widget_wrapper, null) var specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) @@ -144,7 +144,7 @@ abstract class BaseWidget(val context: Context, val id: Int) { } protected val preferedBackgroundAlpha: Int - protected get() = prefs.widgetOpacity + get() = prefs.widgetOpacity init { val app = context.applicationContext as HabitsApplication diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt index effe7f398..97cebe04e 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt @@ -83,10 +83,8 @@ class CheckmarkWidgetView : HabitWidgetView { setShadowAlpha(0x00) } } - // ring.percentage = percentage ring.setPercentage(percentage) ring.setColor(fgColor) - // ring.color = fgColor ring.setBackgroundColor(bgColor) ring.setText(text) label.text = name diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 455518e7a..571360e19 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -101,20 +101,6 @@ abstract class HabitList : Iterable { abstract var primaryOrder: Order abstract var secondaryOrder: Order - // /** - // * Changes the order of the elements on the list. - // * - // * @param order the new order criterion - // */ - // abstract fun setPrimaryOrder(order: Order) - - // /** - // * Changes the previous order of the elements on the list. - // * - // * @param order the new order criterion - // */ - // abstract fun setSecondaryOrder(order: Order) - /** * Returns the index of the given habit in the list, or -1 if the list does * not contain the habit. @@ -172,7 +158,7 @@ abstract class HabitList : Iterable { * * @param habits the list of habits that have been modified. */ - abstract fun update(habits: List?) + abstract fun update(habits: List) /** * Notifies the list that a certain habit has been modified. diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt index d50f5d52d..544d31eb1 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt @@ -33,7 +33,7 @@ import java.util.TimeZone class Timestamp(unixTime: Long) : Comparable { val unixTime: Long - constructor(cal: GregorianCalendar) : this(cal.timeInMillis) {} + constructor(cal: GregorianCalendar) : this(cal.timeInMillis) fun toLocalDate(): LocalDate { val millisSince2000 = unixTime - 946684800000L diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt index 72447862f..129d4971a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -119,16 +119,12 @@ class MemoryHabitList : HabitList { private fun getComparatorByOrder(order: Order): Comparator { val nameComparatorAsc = Comparator { habit1, habit2 -> - habit1.name.compareTo( - habit2.name - ) + habit1.name.compareTo(habit2.name) } val nameComparatorDesc = Comparator { h1: Habit, h2: Habit -> nameComparatorAsc.compare(h2, h1) } val colorComparatorAsc = Comparator { (color1), (color2) -> - color1.compareTo( - color2 - ) + color1.compareTo(color2) } val colorComparatorDesc = Comparator { h1: Habit, h2: Habit -> colorComparatorAsc.compare(h2, h1) } @@ -140,9 +136,7 @@ class MemoryHabitList : HabitList { val scoreComparatorAsc = Comparator { h1: Habit, h2: Habit -> scoreComparatorDesc.compare(h2, h1) } val positionComparator = - Comparator { habit1, habit2 -> - habit1.position.compareTo(habit2.position) - } + Comparator { habit1, habit2 -> habit1.position.compareTo(habit2.position) } val statusComparatorDesc = Comparator { h1: Habit, h2: Habit -> if (h1.isCompletedToday() != h2.isCompletedToday()) { return@Comparator if (h1.isCompletedToday()) -1 else 1 @@ -206,7 +200,7 @@ class MemoryHabitList : HabitList { } @Synchronized - override fun update(habits: List?) { + override fun update(habits: List) { resort() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt index acb32141b..eb08ae3cf 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -31,7 +31,7 @@ import javax.inject.Inject * Implementation of a [HabitList] that is backed by SQLite. */ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory) : HabitList() { - private val repository: Repository + private val repository: Repository = modelFactory.buildHabitListRepository() private val list: MemoryHabitList = MemoryHabitList() private var loaded = false private fun loadRecords() { @@ -197,13 +197,11 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory } @Synchronized - override fun update(habits: List?) { + override fun update(habits: List) { loadRecords() list.update(habits) - for (h in habits!!) { - val record = repository.find( - h!!.id!! - ) ?: continue + for (h in habits) { + val record = repository.find(h.id!!) ?: continue record.copyFrom(h) repository.save(record) } @@ -219,8 +217,4 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory fun reload() { loaded = false } - - init { - repository = modelFactory.buildHabitListRepository() - } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt index 757c4a8f9..eb2bf858f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt @@ -47,11 +47,6 @@ class EntryRecord { } fun toEntry(): Entry { - return Entry( - Timestamp( - timestamp!! - ), - value!! - ) + return Entry(Timestamp(timestamp!!), value!!) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index 942a407a4..3da7b3ed0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -85,8 +85,9 @@ class HabitRecord { @field:Column var uuid: String? = null - fun copyFrom(model: Habit?) { - id = model!!.id + + fun copyFrom(model: Habit) { + id = model.id name = model.name description = model.description highlight = 0 diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt index 1b40d30da..8318429f6 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt @@ -141,7 +141,7 @@ class ReminderScheduler @Inject constructor( ): SchedulerResult fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult? - fun log(componentName: String?, msg: String?) + fun log(componentName: String, msg: String) } enum class SchedulerResult { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index 666947f85..4d6c87fbe 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -42,7 +42,7 @@ class NotificationTray @Inject constructor( private val preferences: Preferences, private val systemTray: SystemTray ) : CommandRunner.Listener, Preferences.Listener { - private val active: HashMap + private val active: HashMap = HashMap() fun cancel(habit: Habit) { val notificationId = getNotificationId(habit) systemTray.removeNotification(notificationId) @@ -86,8 +86,7 @@ class NotificationTray @Inject constructor( } private fun reshowAll() { - for (habit in active.keys) { - val data = active[habit]!! + for ((habit, data) in active.entries) { taskRunner.execute(ShowNotificationTask(habit, data)) } } @@ -101,15 +100,15 @@ class NotificationTray @Inject constructor( reminderTime: Long ) - fun log(msg: String?) + fun log(msg: String) } internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : Task { var todayValue = 0 - private val timestamp: Timestamp - private val reminderTime: Long + private val timestamp: Timestamp = data.timestamp + private val reminderTime: Long = data.reminderTime override fun doInBackground() { val today = getTodayWithOffset() todayValue = habit.computedEntries.get(today).value @@ -172,18 +171,9 @@ class NotificationTray @Inject constructor( val weekday = timestamp.weekday return reminderDays[weekday] } - - init { - timestamp = data.timestamp - reminderTime = data.reminderTime - } } companion object { const val REMINDERS_CHANNEL_ID = "REMINDERS" } - - init { - active = HashMap() - } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index cb3fc9748..26221fd47 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -62,6 +62,7 @@ class HabitCardListCache @Inject constructor( private val data: CacheData private var filteredHabits: HabitList private val taskRunner: TaskRunner + @Synchronized fun cancelTasks() { currentFetchTask?.cancel() @@ -195,6 +196,7 @@ class HabitCardListCache @Inject constructor( val habits: MutableList val checkmarks: HashMap val scores: HashMap + @Synchronized fun copyCheckmarksFrom(oldData: CacheData) { val empty = IntArray(checkmarkCount) @@ -263,18 +265,14 @@ class HabitCardListCache @Inject constructor( if (runner != null) runner!!.publishProgress(this, -1) for (position in newData.habits.indices) { if (isCancelled) return - val (_, _, _, id, _, _, _, _, _, _, _, _, _, _, computedEntries, _, scores) = newData.habits[position] - if (targetId != null && targetId != id) continue - newData.scores[id] = scores[today].value + val habit = newData.habits[position] + if (targetId != null && targetId != habit.id) continue + newData.scores[habit.id] = habit.scores[today].value val list: MutableList = ArrayList() - for ( - (_, value) in computedEntries - .getByInterval(dateFrom, today) - ) { + for ((_, value) in habit.computedEntries.getByInterval(dateFrom, today)) list.add(value) - } val entries = list.toTypedArray() - newData.checkmarks[id] = ArrayUtils.toPrimitive(entries) + newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries) runner!!.publishProgress(this, position) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 0c52a8dcd..515ec6343 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -32,6 +32,7 @@ import java.io.File import java.io.IOException import java.util.LinkedList import javax.inject.Inject +import kotlin.math.roundToInt open class ListHabitsBehavior @Inject constructor( private val habitList: HabitList, @@ -51,14 +52,11 @@ open class ListHabitsBehavior @Inject constructor( val oldValue = entries.get(timestamp!!).value.toDouble() screen.showNumberPicker( oldValue / 1000, - habit.unit, - { newValue: Double -> - val value = Math.round(newValue * 1000).toDouble() - commandRunner.run( - CreateRepetitionCommand(habitList, habit, timestamp, value.toInt()) - ) - } - ) + habit.unit + ) { newValue: Double -> + val value = (newValue * 1000).roundToInt() + commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value)) + } } fun onExportCSV() { @@ -154,7 +152,7 @@ open class ListHabitsBehavior @Inject constructor( callback: NumberPickerCallback ) - fun showSendBugReportToDeveloperScreen(log: String?) + fun showSendBugReportToDeveloperScreen(log: String) fun showSendFileScreen(filename: String) fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt index 268ce4e94..d538f1950 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt @@ -109,10 +109,7 @@ class ListHabitsMenuBehavior @Inject constructor( interface Adapter { fun refresh() - fun setFilter(matcher: HabitMatcher?) - // fun setSecondaryOrder(order: HabitList.Order) - // fun setPrimaryOrder(order: HabitList.Order) - // fun getPrimaryOrder(): HabitList.Order + fun setFilter(matcher: HabitMatcher) var primaryOrder: HabitList.Order var secondaryOrder: HabitList.Order } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 64c3b8f14..ebd033480 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -51,19 +51,15 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( } fun onArchiveHabits() { - commandRunner.run( - ArchiveHabitsCommand(habitList, adapter.selected) - ) + commandRunner.run(ArchiveHabitsCommand(habitList, adapter.selected)) adapter.clearSelection() } fun onChangeColor() { val selected = adapter.selected val (color) = selected[0] - screen.showColorPicker(color) { selectedColor: PaletteColor? -> - commandRunner.run( - ChangeHabitColorCommand(habitList, selected, selectedColor!!) - ) + screen.showColorPicker(color) { selectedColor: PaletteColor -> + commandRunner.run(ChangeHabitColorCommand(habitList, selected, selectedColor)) adapter.clearSelection() } } @@ -73,9 +69,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( screen.showDeleteConfirmationScreen( { adapter.performRemove(selected) - commandRunner.run( - DeleteHabitsCommand(habitList, selected) - ) + commandRunner.run(DeleteHabitsCommand(habitList, selected)) adapter.clearSelection() }, selected.size @@ -88,9 +82,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( } fun onUnarchiveHabits() { - commandRunner.run( - UnarchiveHabitsCommand(habitList, adapter.selected) - ) + commandRunner.run(UnarchiveHabitsCommand(habitList, adapter.selected)) adapter.clearSelection() } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt index 96ffa6cbd..c07e0bf9a 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt @@ -29,16 +29,16 @@ import org.junit.Before import org.junit.Test class RepositoryTest : BaseUnitTest() { - private var repository: Repository? = null - private var db: Database? = null + private lateinit var repository: Repository + private lateinit var db: Database @Before @Throws(Exception::class) override fun setUp() { super.setUp() db = buildMemoryDatabase() - repository = Repository(ThingRecord::class.java, db!!) - db!!.execute("drop table if exists tests") - db!!.execute( + repository = Repository(ThingRecord::class.java, db) + db.execute("drop table if exists tests") + db.execute( "create table tests(" + "id integer not null primary key autoincrement, " + "color_number integer not null, score float not null, " + @@ -49,11 +49,11 @@ class RepositoryTest : BaseUnitTest() { @Test @Throws(Exception::class) fun testFind() { - db!!.execute( + db.execute( "insert into tests(id, color_number, name, score) " + "values (10, 20, 'hello', 8.0)" ) - val record = repository!!.find(10L) + val record = repository.find(10L) Assert.assertNotNull(record) MatcherAssert.assertThat(record!!.id, IsEqual.equalTo(10L)) MatcherAssert.assertThat(record.color, IsEqual.equalTo(20)) @@ -64,38 +64,30 @@ class RepositoryTest : BaseUnitTest() { @Test @Throws(Exception::class) fun testSave_withId() { - val record = ThingRecord() - record.id = 50L - record.color = 10 - record.name = "hello" - record.score = 5.0 - repository!!.save(record) - MatcherAssert.assertThat( - record, - IsEqual.equalTo( - repository!!.find(50L) - ) - ) + val record = ThingRecord().apply { + id = 50L + color = 10 + name = "hello" + score = 5.0 + } + repository.save(record) + MatcherAssert.assertThat(record, IsEqual.equalTo(repository.find(50L))) record.name = "world" record.score = 128.0 - repository!!.save(record) - MatcherAssert.assertThat( - record, - IsEqual.equalTo( - repository!!.find(50L) - ) - ) + repository.save(record) + MatcherAssert.assertThat(record, IsEqual.equalTo(repository.find(50L))) } @Test @Throws(Exception::class) fun testSave_withNull() { - val record = ThingRecord() - record.color = 50 - record.name = null - record.score = 12.0 - repository!!.save(record) - val retrieved = repository!!.find(record.id!!) + val record = ThingRecord().apply { + color = 50 + name = null + score = 12.0 + } + repository.save(record) + val retrieved = repository.find(record.id!!) Assert.assertNotNull(retrieved) Assert.assertNull(retrieved!!.name) MatcherAssert.assertThat(record, IsEqual.equalTo(retrieved)) @@ -104,16 +96,18 @@ class RepositoryTest : BaseUnitTest() { @Test @Throws(Exception::class) fun testSave_withoutId() { - val r1 = ThingRecord() - r1.color = 10 - r1.name = "hello" - r1.score = 16.0 - repository!!.save(r1) - val r2 = ThingRecord() - r2.color = 20 - r2.name = "world" - r2.score = 2.0 - repository!!.save(r2) + val r1 = ThingRecord().apply { + color = 10 + name = "hello" + score = 16.0 + } + repository.save(r1) + val r2 = ThingRecord().apply { + color = 20 + name = "world" + score = 2.0 + } + repository.save(r2) MatcherAssert.assertThat(r1.id, IsEqual.equalTo(1L)) MatcherAssert.assertThat(r2.id, IsEqual.equalTo(2L)) } @@ -121,40 +115,27 @@ class RepositoryTest : BaseUnitTest() { @Test @Throws(Exception::class) fun testRemove() { - val rec1 = ThingRecord() - rec1.color = 10 - rec1.name = "hello" - rec1.score = 16.0 - repository!!.save(rec1) - val rec2 = ThingRecord() - rec2.color = 20 - rec2.name = "world" - rec2.score = 32.0 - repository!!.save(rec2) + val rec1 = ThingRecord().apply { + color = 10 + name = "hello" + score = 16.0 + } + repository.save(rec1) + val rec2 = ThingRecord().apply { + color = 20 + name = "world" + score = 32.0 + } + repository.save(rec2) val id = rec1.id!! - MatcherAssert.assertThat( - rec1, - IsEqual.equalTo( - repository!!.find(id) - ) - ) - MatcherAssert.assertThat( - rec2, - IsEqual.equalTo( - repository!!.find(rec2.id!!) - ) - ) - repository!!.remove(rec1) + MatcherAssert.assertThat(rec1, IsEqual.equalTo(repository.find(id))) + MatcherAssert.assertThat(rec2, IsEqual.equalTo(repository.find(rec2.id!!))) + repository.remove(rec1) MatcherAssert.assertThat(rec1.id, IsEqual.equalTo(null)) - Assert.assertNull(repository!!.find(id)) - MatcherAssert.assertThat( - rec2, - IsEqual.equalTo( - repository!!.find(rec2.id!!) - ) - ) - repository!!.remove(rec1) // should have no effect - Assert.assertNull(repository!!.find(id)) + Assert.assertNull(repository.find(id)) + MatcherAssert.assertThat(rec2, IsEqual.equalTo(repository.find(rec2.id!!))) + repository.remove(rec1) // should have no effect + Assert.assertNull(repository.find(id)) } @Table(name = "tests") diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt index 744b21a62..9c7de4bb3 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt @@ -381,5 +381,5 @@ class EntryListTest { } } - fun day(offset: Int) = DateUtils.getToday().minus(offset)!! + fun day(offset: Int) = DateUtils.getToday().minus(offset) } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecordTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecordTest.kt index 55953f0c8..4fad8f0b6 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecordTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecordTest.kt @@ -31,7 +31,7 @@ import org.junit.Test class HabitRecordTest : BaseUnitTest() { @Test fun testCopyRestore1() { - val original = modelFactory.buildHabit().apply() { + val original = modelFactory.buildHabit().apply { name = "Hello world" question = "Did you greet the world today?" color = PaletteColor(1) @@ -50,7 +50,7 @@ class HabitRecordTest : BaseUnitTest() { @Test fun testCopyRestore2() { - val original = modelFactory.buildHabit().apply() { + val original = modelFactory.buildHabit().apply { name = "Hello world" question = "Did you greet the world today?" color = PaletteColor(5) diff --git a/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/BaseApplicationTest.kt b/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/BaseApplicationTest.kt index 547f89fce..7b5c6d27b 100644 --- a/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/BaseApplicationTest.kt +++ b/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/BaseApplicationTest.kt @@ -19,13 +19,13 @@ package org.isoron.uhabits.sync.app +import com.nhaarman.mockitokotlin2.mock import io.ktor.application.* import org.isoron.uhabits.sync.server.* -import org.mockito.Mockito.* open class BaseApplicationTest { - protected val server: AbstractSyncServer = mock(AbstractSyncServer::class.java) + protected val server: AbstractSyncServer = mock() protected fun app(): Application.() -> Unit = { SyncApplication(server).apply { diff --git a/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/StorageModuleTest.kt b/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/StorageModuleTest.kt index 4165bab5e..7609587e0 100644 --- a/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/StorageModuleTest.kt +++ b/uhabits-server/src/test/kotlin/org/isoron/uhabits/sync/app/StorageModuleTest.kt @@ -19,13 +19,13 @@ package org.isoron.uhabits.sync.app +import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.coroutines.* import org.isoron.uhabits.sync.* import org.junit.Test -import org.mockito.Mockito.* import kotlin.test.* class StorageModuleTest : BaseApplicationTest() { From 9a721415675794e04c2e62316a0ec53f76d60917 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 22:09:33 +0100 Subject: [PATCH 26/35] Convert PerformanceTest --- .../uhabits/performance/PerformanceTest.java | 78 ------------------- .../uhabits/performance/PerformanceTest.kt | 69 ++++++++++++++++ 2 files changed, 69 insertions(+), 78 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java deleted file mode 100644 index 7c7402a8e..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.performance; - -import androidx.test.ext.junit.runners.*; -import androidx.test.filters.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.models.sqlite.*; -import org.junit.*; -import org.junit.runner.*; - -import static org.isoron.uhabits.core.models.Timestamp.*; - -@RunWith(AndroidJUnit4.class) -@MediumTest -public class PerformanceTest extends BaseAndroidTest -{ - private Habit habit; - - @Override - public void setUp() - { - super.setUp(); - habit = fixtures.createLongHabit(); - } - - @Ignore - @Test(timeout = 5000) - public void benchmarkCreateHabitCommand() - { - Database db = ((SQLModelFactory) modelFactory).getDatabase(); - db.beginTransaction(); - for (int i = 0; i < 1_000; i++) - { - Habit model = modelFactory.buildHabit(); - new CreateHabitCommand(modelFactory, habitList, model).run(); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - @Ignore - @Test(timeout = 5000) - public void benchmarkCreateRepetitionCommand() - { - Database db = ((SQLModelFactory) modelFactory).getDatabase(); - db.beginTransaction(); - Habit habit = fixtures.createEmptyHabit(); - for (int i = 0; i < 5_000; i++) - { - Timestamp timestamp = new Timestamp(i * DAY_LENGTH); - new CreateRepetitionCommand(habitList, habit, timestamp, 1).run(); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt new file mode 100644 index 000000000..10ad56057 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.performance + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.isoron.uhabits.BaseAndroidTest +import org.isoron.uhabits.core.commands.CreateHabitCommand +import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.models.Timestamp.Companion.DAY_LENGTH +import org.isoron.uhabits.core.models.sqlite.SQLModelFactory +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class PerformanceTest : BaseAndroidTest() { + private var habit: Habit? = null + override fun setUp() { + super.setUp() + habit = fixtures.createLongHabit() + } + + @Ignore + @Test(timeout = 5000) + fun benchmarkCreateHabitCommand() { + val db = (modelFactory as SQLModelFactory).database + db.beginTransaction() + for (i in 0..999) { + val model = modelFactory.buildHabit() + CreateHabitCommand(modelFactory, habitList, model).run() + } + db.setTransactionSuccessful() + db.endTransaction() + } + + @Ignore + @Test(timeout = 5000) + fun benchmarkCreateRepetitionCommand() { + val db = (modelFactory as SQLModelFactory).database + db.beginTransaction() + val habit = fixtures.createEmptyHabit() + for (i in 0..4999) { + val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) + CreateRepetitionCommand(habitList, habit, timestamp, 1).run() + } + db.setTransactionSuccessful() + db.endTransaction() + } +} From 2bfbff9b14bc8009e1fd8fa6e06a077b4fa791e5 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 22:19:57 +0100 Subject: [PATCH 27/35] Convert ExportDBTask and ImportDataTask --- .../uhabits/tasks/AndroidTaskRunner.java | 2 +- .../isoron/uhabits/tasks/ExportDBTask.java | 82 -------------- .../org/isoron/uhabits/tasks/ExportDBTask.kt | 51 +++++++++ .../isoron/uhabits/tasks/ImportDataTask.java | 101 ------------------ .../isoron/uhabits/tasks/ImportDataTask.kt | 66 ++++++++++++ 5 files changed, 118 insertions(+), 184 deletions(-) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java index 654e9bdef..f1348cec5 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java @@ -36,7 +36,7 @@ public class AndroidTaskRunner implements TaskRunner private final HashMap taskToAsyncTask; - private LinkedList listeners; + private final LinkedList listeners; public AndroidTaskRunner() { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java deleted file mode 100644 index f2082d4c9..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.tasks; - -import android.content.*; - -import androidx.annotation.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.inject.*; -import org.isoron.uhabits.utils.*; - -import java.io.*; - -public class ExportDBTask implements Task -{ - private String filename; - - @NonNull - private Context context; - - private AndroidDirFinder system; - - @NonNull - private final Listener listener; - - public ExportDBTask(@AppContext @NonNull Context context, - @NonNull AndroidDirFinder system, - @NonNull Listener listener) - { - this.system = system; - this.listener = listener; - this.context = context; - } - - @Override - public void doInBackground() - { - filename = null; - - try - { - File dir = system.getFilesDir("Backups"); - if (dir == null) return; - - filename = DatabaseUtils.saveDatabaseCopy(context, dir); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - @Override - public void onPostExecute() - { - listener.onExportDBFinished(filename); - } - - public interface Listener - { - void onExportDBFinished(@Nullable String filename); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.kt b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.kt new file mode 100644 index 000000000..45db42a76 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.tasks + +import android.content.Context +import org.isoron.uhabits.AndroidDirFinder +import org.isoron.uhabits.core.tasks.Task +import org.isoron.uhabits.inject.AppContext +import org.isoron.uhabits.utils.DatabaseUtils.saveDatabaseCopy +import java.io.IOException + +class ExportDBTask( + @param:AppContext private val context: Context, + private val system: AndroidDirFinder, + private val listener: Listener +) : Task { + private var filename: String? = null + override fun doInBackground() { + filename = null + filename = try { + val dir = system.getFilesDir("Backups") ?: return + saveDatabaseCopy(context, dir) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun onPostExecute() { + listener.onExportDBFinished(filename) + } + + fun interface Listener { + fun onExportDBFinished(filename: String?) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java deleted file mode 100644 index 98ee48eff..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.tasks; - -import android.util.*; - -import androidx.annotation.NonNull; - -import org.isoron.uhabits.core.io.*; -import org.isoron.uhabits.core.models.ModelFactory; -import org.isoron.uhabits.core.models.sqlite.SQLModelFactory; -import org.isoron.uhabits.core.tasks.*; - -import java.io.*; - -public class ImportDataTask implements Task -{ - public static final int FAILED = 3; - - public static final int NOT_RECOGNIZED = 2; - - public static final int SUCCESS = 1; - - private int result; - - @NonNull - private final File file; - - private GenericImporter importer; - - private SQLModelFactory modelFactory; - - @NonNull - private final Listener listener; - - public ImportDataTask(@NonNull GenericImporter importer, - @NonNull ModelFactory modelFactory, - @NonNull File file, - @NonNull Listener listener) - { - this.importer = importer; - this.modelFactory = (SQLModelFactory) modelFactory; - this.listener = listener; - this.file = file; - } - - @Override - public void doInBackground() - { - modelFactory.getDatabase().beginTransaction(); - - try - { - if (importer.canHandle(file)) - { - importer.importHabitsFromFile(file); - result = SUCCESS; - modelFactory.getDatabase().setTransactionSuccessful(); - } - else - { - result = NOT_RECOGNIZED; - } - } - catch (Exception e) - { - result = FAILED; - Log.e("ImportDataTask", "Import failed", e); - } - - modelFactory.getDatabase().endTransaction(); - } - - @Override - public void onPostExecute() - { - listener.onImportDataFinished(result); - } - - public interface Listener - { - void onImportDataFinished(int result); - } -} \ No newline at end of file diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.kt b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.kt new file mode 100644 index 000000000..6f804233a --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.tasks + +import android.util.Log +import org.isoron.uhabits.core.io.GenericImporter +import org.isoron.uhabits.core.models.ModelFactory +import org.isoron.uhabits.core.models.sqlite.SQLModelFactory +import org.isoron.uhabits.core.tasks.Task +import java.io.File + +class ImportDataTask( + private val importer: GenericImporter, + modelFactory: ModelFactory, + private val file: File, + private val listener: Listener +) : Task { + private var result = 0 + private val modelFactory: SQLModelFactory = modelFactory as SQLModelFactory + override fun doInBackground() { + modelFactory.database.beginTransaction() + try { + if (importer.canHandle(file)) { + importer.importHabitsFromFile(file) + result = SUCCESS + modelFactory.database.setTransactionSuccessful() + } else { + result = NOT_RECOGNIZED + } + } catch (e: Exception) { + result = FAILED + Log.e("ImportDataTask", "Import failed", e) + } + modelFactory.database.endTransaction() + } + + override fun onPostExecute() { + listener.onImportDataFinished(result) + } + + fun interface Listener { + fun onImportDataFinished(result: Int) + } + + companion object { + const val FAILED = 3 + const val NOT_RECOGNIZED = 2 + const val SUCCESS = 1 + } +} From 6485c3efee4547395f1072608e04951efc8729f1 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 22:37:25 +0100 Subject: [PATCH 28/35] Convert ExportCSVTask --- .../uhabits/core/tasks/ExportCSVTask.java | 81 ------------------- .../uhabits/core/tasks/ExportCSVTask.kt | 49 +++++++++++ 2 files changed, 49 insertions(+), 81 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java deleted file mode 100644 index e1120a87c..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.tasks; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.io.*; -import org.isoron.uhabits.core.models.*; - -import java.io.*; -import java.util.*; - -public class ExportCSVTask implements Task -{ - private String archiveFilename; - - @NonNull - private final List selectedHabits; - - private File outputDir; - - @NonNull - private final ExportCSVTask.Listener listener; - - @NonNull - private final HabitList habitList; - - public ExportCSVTask(@NonNull HabitList habitList, - @NonNull List selectedHabits, - @NonNull File outputDir, - @NonNull Listener listener) - { - this.listener = listener; - this.habitList = habitList; - this.selectedHabits = selectedHabits; - this.outputDir = outputDir; - } - - @Override - public void doInBackground() - { - try - { - HabitsCSVExporter exporter; - exporter = new HabitsCSVExporter(habitList, selectedHabits, outputDir); - archiveFilename = exporter.writeArchive(); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - @Override - public void onPostExecute() - { - listener.onExportCSVFinished(archiveFilename); - } - - public interface Listener - { - void onExportCSVFinished(@Nullable String archiveFilename); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.kt new file mode 100644 index 000000000..b3cbdb2e7 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/ExportCSVTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.tasks + +import org.isoron.uhabits.core.io.HabitsCSVExporter +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import java.io.File + +class ExportCSVTask( + private val habitList: HabitList, + private val selectedHabits: List, + private val outputDir: File, + private val listener: Listener +) : Task { + private var archiveFilename: String? = null + override fun doInBackground() { + try { + val exporter = HabitsCSVExporter(habitList, selectedHabits, outputDir) + archiveFilename = exporter.writeArchive() + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onPostExecute() { + listener.onExportCSVFinished(archiveFilename) + } + + fun interface Listener { + fun onExportCSVFinished(archiveFilename: String?) + } +} From 8131d37d8eafa71d7311e9f1ee61f5904532090e Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 22:40:41 +0100 Subject: [PATCH 29/35] Convert SingleThreadTaskRunner --- .../core/tasks/SingleThreadTaskRunner.java | 65 ------------------- .../core/tasks/SingleThreadTaskRunner.kt | 51 +++++++++++++++ 2 files changed, 51 insertions(+), 65 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.java create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.java deleted file mode 100644 index df952496c..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.tasks; - -import java.util.*; - -public class SingleThreadTaskRunner implements TaskRunner -{ - private List listeners = new LinkedList<>(); - - @Override - public void addListener(Listener listener) - { - listeners.add(listener); - } - - @Override - public void execute(Task task) - { - for(Listener l : listeners) l.onTaskStarted(task); - if(!task.isCanceled()) - { - task.onAttached(this); - task.onPreExecute(); - task.doInBackground(); - task.onPostExecute(); - } - for(Listener l : listeners) l.onTaskFinished(task); - } - - @Override - public int getActiveTaskCount() - { - return 0; - } - - @Override - public void publishProgress(Task task, int progress) - { - task.onProgressUpdate(progress); - } - - @Override - public void removeListener(Listener listener) - { - listeners.remove(listener); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt new file mode 100644 index 000000000..c2a151a7b --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.tasks + +import java.util.LinkedList + +class SingleThreadTaskRunner : TaskRunner { + private val listeners: MutableList = LinkedList() + override fun addListener(listener: TaskRunner.Listener) { + listeners.add(listener) + } + + override fun execute(task: Task) { + for (l in listeners) l.onTaskStarted(task) + if (!task.isCanceled) { + task.onAttached(this) + task.onPreExecute() + task.doInBackground() + task.onPostExecute() + } + for (l in listeners) l.onTaskFinished(task) + } + + override fun getActiveTaskCount(): Int { + return 0 + } + + override fun publishProgress(task: Task, progress: Int) { + task.onProgressUpdate(progress) + } + + override fun removeListener(listener: TaskRunner.Listener) { + listeners.remove(listener) + } +} From 7d361b22035deb99cc2145beeb5be34c9acf8ae1 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 22:47:40 +0100 Subject: [PATCH 30/35] Convert Task and TaskRunner --- .../common/views/TaskProgressBar.kt | 4 +-- .../uhabits/tasks/AndroidTaskRunner.java | 12 ++++--- .../core/tasks/SingleThreadTaskRunner.kt | 7 ++-- .../uhabits/core/tasks/{Task.java => Task.kt} | 35 +++++++------------ .../tasks/{TaskRunner.java => TaskRunner.kt} | 32 +++++++---------- .../screens/habits/list/HabitCardListCache.kt | 5 +-- 6 files changed, 40 insertions(+), 55 deletions(-) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/{Task.java => Task.kt} (66%) rename uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/{TaskRunner.java => TaskRunner.kt} (67%) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TaskProgressBar.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TaskProgressBar.kt index aa1b74a97..548fdc5f5 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TaskProgressBar.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TaskProgressBar.kt @@ -40,8 +40,8 @@ class TaskProgressBar( isIndeterminate = true } - override fun onTaskStarted(task: Task?) = update() - override fun onTaskFinished(task: Task?) = update() + override fun onTaskStarted(task: Task) = update() + override fun onTaskFinished(task: Task) = update() override fun onAttachedToWindow() { super.onAttachedToWindow() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java index f1348cec5..c5bd08d50 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java @@ -23,6 +23,8 @@ import android.os.*; import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.tasks.*; +import org.isoron.uhabits.core.tasks.Task; +import org.jetbrains.annotations.NotNull; import java.util.*; @@ -53,18 +55,19 @@ public class AndroidTaskRunner implements TaskRunner } @Override - public void addListener(Listener listener) + public void addListener(@NotNull Listener listener) { listeners.add(listener); } @Override - public void execute(Task task) + public void execute(@NotNull Task task) { task.onAttached(this); new CustomAsyncTask(task).execute(); } + @Override public int getActiveTaskCount() { @@ -72,7 +75,7 @@ public class AndroidTaskRunner implements TaskRunner } @Override - public void publishProgress(Task task, int progress) + public void publishProgress(@NotNull Task task, int progress) { CustomAsyncTask asyncTask = taskToAsyncTask.get(task); if (asyncTask == null) return; @@ -80,11 +83,12 @@ public class AndroidTaskRunner implements TaskRunner } @Override - public void removeListener(Listener listener) + public void removeListener(@NotNull Listener listener) { listeners.remove(listener); } + private class CustomAsyncTask extends AsyncTask { private final Task task; diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt index c2a151a7b..9f7eaebb9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/SingleThreadTaskRunner.kt @@ -21,6 +21,9 @@ package org.isoron.uhabits.core.tasks import java.util.LinkedList class SingleThreadTaskRunner : TaskRunner { + override val activeTaskCount: Int + get() = 0 + private val listeners: MutableList = LinkedList() override fun addListener(listener: TaskRunner.Listener) { listeners.add(listener) @@ -37,10 +40,6 @@ class SingleThreadTaskRunner : TaskRunner { for (l in listeners) l.onTaskFinished(task) } - override fun getActiveTaskCount(): Int { - return 0 - } - override fun publishProgress(task: Task, progress: Int) { task.onProgressUpdate(progress) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.kt similarity index 66% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.kt index 1283428d8..cc3a82cf0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/Task.kt @@ -16,27 +16,16 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ - -package org.isoron.uhabits.core.tasks; - -import androidx.annotation.*; - -public interface Task -{ - default void cancel() {} - - default boolean isCanceled() - { - return false; - } - - void doInBackground(); - - default void onAttached(@NonNull TaskRunner runner) {} - - default void onPostExecute() {} - - default void onPreExecute() {} - - default void onProgressUpdate(int value) {} +package org.isoron.uhabits.core.tasks + +fun interface Task { + fun cancel() {} + val isCanceled: Boolean + get() = false + + fun doInBackground() + fun onAttached(runner: TaskRunner) {} + fun onPostExecute() {} + fun onPreExecute() {} + fun onProgressUpdate(value: Int) {} } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.kt similarity index 67% rename from uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.java rename to uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.kt index 60837226d..a59ff1d7b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.java +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/tasks/TaskRunner.kt @@ -16,25 +16,17 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ - -package org.isoron.uhabits.core.tasks; - -public interface TaskRunner -{ - void addListener(Listener listener); - - void removeListener(Listener listener); - - void execute(Task task); - - void publishProgress(Task task, int progress); - - int getActiveTaskCount(); - - interface Listener - { - void onTaskStarted(Task task); - - void onTaskFinished(Task task); +package org.isoron.uhabits.core.tasks + +interface TaskRunner { + fun addListener(listener: Listener) + fun removeListener(listener: Listener) + fun execute(task: Task) + fun publishProgress(task: Task, progress: Int) + val activeTaskCount: Int + + interface Listener { + fun onTaskStarted(task: Task) + fun onTaskFinished(task: Task) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 26221fd47..e55928f4b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -136,8 +136,9 @@ class HabitCardListCache @Inject constructor( @Synchronized fun refreshAllHabits() { if (currentFetchTask != null) currentFetchTask!!.cancel() - currentFetchTask = RefreshTask() - taskRunner.execute(currentFetchTask) + val task = RefreshTask() + currentFetchTask = task + taskRunner.execute(task) } @Synchronized From 22dcd9f7aeeb28fc86331bd2009b026e25914ba2 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 23:01:36 +0100 Subject: [PATCH 31/35] Convert AndroidTaskRunner --- .../uhabits/tasks/AndroidTaskRunner.java | 147 ------------------ .../isoron/uhabits/tasks/AndroidTaskRunner.kt | 100 ++++++++++++ 2 files changed, 100 insertions(+), 147 deletions(-) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java deleted file mode 100644 index c5bd08d50..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.tasks; - -import android.os.*; - -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.tasks.Task; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -import dagger.Module; -import dagger.Provides; - -@Module -public class AndroidTaskRunner implements TaskRunner -{ - private final LinkedList activeTasks; - - private final HashMap taskToAsyncTask; - - private final LinkedList listeners; - - public AndroidTaskRunner() - { - activeTasks = new LinkedList<>(); - taskToAsyncTask = new HashMap<>(); - listeners = new LinkedList<>(); - } - - @Provides - @AppScope - public static TaskRunner provideTaskRunner() - { - return new AndroidTaskRunner(); - } - - @Override - public void addListener(@NotNull Listener listener) - { - listeners.add(listener); - } - - @Override - public void execute(@NotNull Task task) - { - task.onAttached(this); - new CustomAsyncTask(task).execute(); - } - - - @Override - public int getActiveTaskCount() - { - return activeTasks.size(); - } - - @Override - public void publishProgress(@NotNull Task task, int progress) - { - CustomAsyncTask asyncTask = taskToAsyncTask.get(task); - if (asyncTask == null) return; - asyncTask.publish(progress); - } - - @Override - public void removeListener(@NotNull Listener listener) - { - listeners.remove(listener); - } - - - private class CustomAsyncTask extends AsyncTask - { - private final Task task; - private boolean isCancelled = false; - - public CustomAsyncTask(Task task) - { - this.task = task; - } - - public Task getTask() - { - return task; - } - - public void publish(int progress) - { - publishProgress(progress); - } - - @Override - protected Void doInBackground(Void... params) - { - if(isCancelled) return null; - task.doInBackground(); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) - { - if(isCancelled) return; - task.onPostExecute(); - activeTasks.remove(this); - taskToAsyncTask.remove(task); - for (Listener l : listeners) l.onTaskFinished(task); - } - - @Override - protected void onPreExecute() - { - isCancelled = task.isCanceled(); - if(isCancelled) return; - for (Listener l : listeners) l.onTaskStarted(task); - activeTasks.add(this); - taskToAsyncTask.put(task, this); - task.onPreExecute(); - } - - @Override - protected void onProgressUpdate(Integer... values) - { - task.onProgressUpdate(values[0]); - } - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt new file mode 100644 index 000000000..f2f519ca0 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.tasks + +import android.os.AsyncTask +import dagger.Module +import dagger.Provides +import org.isoron.uhabits.core.AppScope +import org.isoron.uhabits.core.tasks.Task +import org.isoron.uhabits.core.tasks.TaskRunner +import java.util.HashMap +import java.util.LinkedList + +// TODO: @Module not needed? +@Module +class AndroidTaskRunner : TaskRunner { + private val activeTasks: LinkedList = LinkedList() + private val taskToAsyncTask: HashMap = HashMap() + private val listeners: LinkedList = LinkedList() + override fun addListener(listener: TaskRunner.Listener) { + listeners.add(listener) + } + + override fun execute(task: Task) { + task.onAttached(this) + CustomAsyncTask(task).execute() + } + + override val activeTaskCount: Int + get() = activeTasks.size + + override fun publishProgress(task: Task, progress: Int) { + val asyncTask = taskToAsyncTask[task] ?: return + asyncTask.publish(progress) + } + + override fun removeListener(listener: TaskRunner.Listener) { + listeners.remove(listener) + } + + private inner class CustomAsyncTask(val task: Task) : AsyncTask() { + + fun publish(progress: Int) { + publishProgress(progress) + } + + override fun doInBackground(vararg params: Void?): Void? { + if (isCancelled) return null + task.doInBackground() + return null + } + + override fun onPostExecute(aVoid: Void?) { + if (isCancelled) return + task.onPostExecute() + activeTasks.remove(this) + taskToAsyncTask.remove(task) + for (l in listeners) l.onTaskFinished(task) + } + + override fun onPreExecute() { + // isCancelled = task.isCanceled + if (isCancelled) return + for (l in listeners) l.onTaskStarted(task) + activeTasks.add(this) + taskToAsyncTask[task] = this + task.onPreExecute() + } + + override fun onProgressUpdate(vararg values: Int?) { + values[0]?.let { task.onProgressUpdate(it) } + } + } + + @Module + companion object { + @JvmStatic + @Provides + @AppScope + fun provideTaskRunner(): TaskRunner { + return AndroidTaskRunner() + } + } +} From 0fc9bb57aedaf03893efabc5857a7257a3a14cb6 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 23:07:42 +0100 Subject: [PATCH 32/35] Convert BaseViewTest --- .../java/org/isoron/uhabits/BaseViewTest.java | 198 ------------------ .../java/org/isoron/uhabits/BaseViewTest.kt | 185 ++++++++++++++++ 2 files changed, 185 insertions(+), 198 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java deleted file mode 100644 index 66c07ffcc..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits; - -import android.graphics.*; -import android.view.*; -import android.widget.*; - -import androidx.annotation.*; -import androidx.test.platform.app.*; - -import org.isoron.uhabits.utils.*; -import org.isoron.uhabits.widgets.*; - -import java.io.*; -import java.util.*; - -import static android.view.View.MeasureSpec.*; - -public class BaseViewTest extends BaseAndroidTest -{ - public double similarityCutoff = 0.00018; - - @Override - public void setUp() - { - super.setUp(); - } - - protected void assertRenders(View view, String expectedImagePath) - throws IOException - { - Bitmap actual = renderView(view); - if(actual == null) throw new IllegalStateException("actual is null"); - assertRenders(actual, expectedImagePath); - } - - protected void assertRenders(Bitmap actual, String expectedImagePath) throws IOException { - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - expectedImagePath = "views/" + expectedImagePath; - try - { - Bitmap expected = getBitmapFromAssets(expectedImagePath); - double distance = distance(actual, expected); - if (distance > similarityCutoff) - { - saveBitmap(expectedImagePath, ".expected", expected); - String path = saveBitmap(expectedImagePath, "", actual); - fail(String.format("Image differs from expected " + - "(distance=%f). Actual rendered " + - "image saved to %s", distance, path)); - } - - expected.recycle(); - } - catch (IOException e) - { - String path = saveBitmap(expectedImagePath, "", actual); - fail(String.format("Could not open expected image. Actual " + - "rendered image saved to %s", path)); - throw e; - } - } - - @NonNull - protected FrameLayout convertToView(BaseWidget widget, - int width, - int height) - { - widget.setDimensions( - new WidgetDimensions(width, height, width, height)); - FrameLayout view = new FrameLayout(targetContext); - RemoteViews remoteViews = widget.getPortraitRemoteViews(); - view.addView(remoteViews.apply(targetContext, view)); - measureView(view, width, height); - return view; - } - - protected float dpToPixels(int dp) - { - return InterfaceUtils.dpToPixels(targetContext, dp); - } - - protected void measureView(View view, float width, float height) - { - int specWidth = makeMeasureSpec((int) width, View.MeasureSpec.EXACTLY); - int specHeight = makeMeasureSpec((int) height, View.MeasureSpec.EXACTLY); - - view.setLayoutParams(new ViewGroup.LayoutParams((int) width, (int) height)); - view.measure(specWidth, specHeight); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - } - - protected void skipAnimation(View view) - { - ViewPropertyAnimator animator = view.animate(); - animator.setDuration(0); - animator.start(); - } - - private int[] colorToArgb(int c1) - { - return new int[]{ - (c1 >> 24) & 0xff, //alpha - (c1 >> 16) & 0xff, //red - (c1 >> 8) & 0xff, //green - (c1) & 0xff //blue - }; - } - - private double distance(Bitmap b1, Bitmap b2) - { - if (b1.getWidth() != b2.getWidth()) return 1.0; - if (b1.getHeight() != b2.getHeight()) return 1.0; - - Random random = new Random(); - - double distance = 0.0; - for (int x = 0; x < b1.getWidth(); x++) - { - for (int y = 0; y < b1.getHeight(); y++) - { - if (random.nextInt(4) != 0) continue; - - int[] argb1 = colorToArgb(b1.getPixel(x, y)); - int[] argb2 = colorToArgb(b2.getPixel(x, y)); - distance += Math.abs(argb1[0] - argb2[0]); - distance += Math.abs(argb1[1] - argb2[1]); - distance += Math.abs(argb1[2] - argb2[2]); - distance += Math.abs(argb1[3] - argb2[3]); - } - } - - distance /= 255.0 * 16 * b1.getWidth() * b1.getHeight(); - return distance; - } - - private Bitmap getBitmapFromAssets(String path) throws IOException - { - InputStream stream = testContext.getAssets().open(path); - return BitmapFactory.decodeStream(stream); - } - - private String saveBitmap(String filename, String suffix, Bitmap bitmap) - throws IOException - { - File dir = FileUtils.getSDCardDir("test-screenshots"); - if (dir == null) - dir = new AndroidDirFinder(targetContext).getFilesDir("test-screenshots"); - if (dir == null) throw new RuntimeException( - "Could not find suitable dir for screenshots"); - - filename = filename.replaceAll("\\.png$", suffix + ".png"); - String absolutePath = - String.format("%s/%s", dir.getAbsolutePath(), filename); - - File parent = new File(absolutePath).getParentFile(); - if (!parent.exists() && !parent.mkdirs()) throw new RuntimeException( - String.format("Could not create dir: %s", - parent.getAbsolutePath())); - - FileOutputStream out = new FileOutputStream(absolutePath); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); - - return absolutePath; - } - - public Bitmap renderView(View view) - { - int width = view.getMeasuredWidth(); - int height = view.getMeasuredHeight(); - if(view.isLayoutRequested()) - measureView(view, width, height); - - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - view.invalidate(); - view.draw(canvas); - return bitmap; - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt new file mode 100644 index 000000000..c5cd6c94c --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.view.View +import android.view.View.MeasureSpec +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.test.platform.app.InstrumentationRegistry +import org.isoron.uhabits.utils.FileUtils.getSDCardDir +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.widgets.BaseWidget +import org.isoron.uhabits.widgets.WidgetDimensions +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Random + +open class BaseViewTest : BaseAndroidTest() { + var similarityCutoff = 0.00018 + override fun setUp() { + super.setUp() + } + + @Throws(IOException::class) + protected fun assertRenders(view: View, expectedImagePath: String) { + val actual = renderView(view) + assertRenders(actual, expectedImagePath) + } + + @Throws(IOException::class) + protected fun assertRenders(actual: Bitmap, expectedImagePath: String) { + var expectedImagePath = expectedImagePath + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + expectedImagePath = "views/$expectedImagePath" + try { + val expected = getBitmapFromAssets(expectedImagePath) + val distance = distance(actual, expected) + if (distance > similarityCutoff) { + saveBitmap(expectedImagePath, ".expected", expected) + val path = saveBitmap(expectedImagePath, "", actual) + fail( + String.format( + "Image differs from expected " + + "(distance=%f). Actual rendered " + + "image saved to %s", + distance, + path + ) + ) + } + expected.recycle() + } catch (e: IOException) { + val path = saveBitmap(expectedImagePath, "", actual) + fail( + String.format( + "Could not open expected image. Actual " + + "rendered image saved to %s", + path + ) + ) + throw e + } + } + + protected fun convertToView( + widget: BaseWidget, + width: Int, + height: Int + ): FrameLayout { + widget.setDimensions( + WidgetDimensions(width, height, width, height) + ) + val view = FrameLayout(targetContext) + val remoteViews = widget.portraitRemoteViews + view.addView(remoteViews.apply(targetContext, view)) + measureView(view, width.toFloat(), height.toFloat()) + return view + } + + protected fun dpToPixels(dp: Int): Float { + return dpToPixels(targetContext, dp.toFloat()) + } + + protected fun measureView(view: View, width: Float, height: Float) { + val specWidth = MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY) + val specHeight = MeasureSpec.makeMeasureSpec(height.toInt(), MeasureSpec.EXACTLY) + view.layoutParams = ViewGroup.LayoutParams(width.toInt(), height.toInt()) + view.measure(specWidth, specHeight) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + } + + protected fun skipAnimation(view: View) { + val animator = view.animate() + animator.duration = 0 + animator.start() + } + + private fun colorToArgb(c1: Int): IntArray { + return intArrayOf( + c1 shr 24 and 0xff, // alpha + c1 shr 16 and 0xff, // red + c1 shr 8 and 0xff, // green + c1 and 0xff // blue + ) + } + + private fun distance(b1: Bitmap, b2: Bitmap): Double { + if (b1.width != b2.width) return 1.0 + if (b1.height != b2.height) return 1.0 + val random = Random() + var distance = 0.0 + for (x in 0 until b1.width) { + for (y in 0 until b1.height) { + if (random.nextInt(4) != 0) continue + val argb1 = colorToArgb(b1.getPixel(x, y)) + val argb2 = colorToArgb(b2.getPixel(x, y)) + distance += Math.abs(argb1[0] - argb2[0]).toDouble() + distance += Math.abs(argb1[1] - argb2[1]).toDouble() + distance += Math.abs(argb1[2] - argb2[2]).toDouble() + distance += Math.abs(argb1[3] - argb2[3]).toDouble() + } + } + distance /= 255.0 * 16 * b1.width * b1.height + return distance + } + + @Throws(IOException::class) + private fun getBitmapFromAssets(path: String): Bitmap { + val stream = testContext.assets.open(path) + return BitmapFactory.decodeStream(stream) + } + + @Throws(IOException::class) + private fun saveBitmap(filename: String, suffix: String, bitmap: Bitmap): String { + var filename = filename + var dir = getSDCardDir("test-screenshots") + if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots") + if (dir == null) throw RuntimeException( + "Could not find suitable dir for screenshots" + ) + filename = filename.replace("\\.png$".toRegex(), "$suffix.png") + val absolutePath = String.format("%s/%s", dir.absolutePath, filename) + val parent = File(absolutePath).parentFile + if (!parent.exists() && !parent.mkdirs()) throw RuntimeException( + String.format( + "Could not create dir: %s", + parent.absolutePath + ) + ) + val out = FileOutputStream(absolutePath) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + return absolutePath + } + + fun renderView(view: View): Bitmap { + val width = view.measuredWidth + val height = view.measuredHeight + if (view.isLayoutRequested) measureView(view, width.toFloat(), height.toFloat()) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + view.invalidate() + view.draw(canvas) + return bitmap + } +} From f882e18be91909358593900369200dbce400d58b Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 23:11:05 +0100 Subject: [PATCH 33/35] Convert BaseUserInterfaceTest --- .../isoron/uhabits/BaseUserInterfaceTest.java | 133 ------------------ .../isoron/uhabits/BaseUserInterfaceTest.kt | 118 ++++++++++++++++ .../uhabits/acceptance/steps/BackupSteps.kt | 2 +- 3 files changed, 119 insertions(+), 134 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java deleted file mode 100644 index 9d9d5808b..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits; - -import android.content.*; - -import androidx.test.uiautomator.*; - -import com.linkedin.android.testbutler.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.preferences.*; -import org.isoron.uhabits.core.ui.screens.habits.list.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.inject.*; -import org.junit.*; - -import static androidx.test.core.app.ApplicationProvider.*; -import static androidx.test.platform.app.InstrumentationRegistry.*; -import static androidx.test.uiautomator.UiDevice.*; - -public class BaseUserInterfaceTest -{ - private static final String PKG = "org.isoron.uhabits"; - public static final String EMPTY_DESCRIPTION_HABIT_NAME = "Read books"; - - public static UiDevice device; - - private HabitsApplicationComponent component; - - private HabitList habitList; - - private Preferences prefs; - - private HabitFixtures fixtures; - - private HabitCardListCache cache; - - public static void startActivity(Class cls) - { - Intent intent = new Intent(); - intent.setComponent(new ComponentName(PKG, cls.getCanonicalName())); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getApplicationContext().startActivity(intent); - } - - @Before - public void setUp() throws Exception - { - device = getInstance(getInstrumentation()); - TestButler.setup(getApplicationContext()); - TestButler.verifyAnimationsDisabled(getApplicationContext()); - - HabitsApplication app = - (HabitsApplication) getApplicationContext().getApplicationContext(); - component = app.getComponent(); - habitList = component.getHabitList(); - prefs = component.getPreferences(); - cache = component.getHabitCardListCache(); - fixtures = new HabitFixtures(component.getModelFactory(), habitList); - resetState(); - } - - @After - public void tearDown() throws Exception - { - for (int i = 0; i < 10; i++) device.pressBack(); - TestButler.teardown(getApplicationContext()); - } - - private void resetState() throws Exception - { - prefs.clear(); - prefs.setFirstRun(false); - prefs.updateLastHint(100, DateUtils.getToday()); - habitList.removeAll(); - cache.refreshAllHabits(); - Thread.sleep(1000); - - Habit h1 = fixtures.createEmptyHabit(); - h1.setName("Wake up early"); - h1.setQuestion("Did you wake up early today?"); - h1.setDescription("test description 1"); - h1.setColor(new PaletteColor(5)); - habitList.update(h1); - - Habit h2 = fixtures.createShortHabit(); - h2.setName("Track time"); - h2.setQuestion("Did you track your time?"); - h2.setDescription("test description 2"); - h2.setColor(new PaletteColor(5)); - habitList.update(h2); - - Habit h3 = fixtures.createLongHabit(); - h3.setName("Meditate"); - h3.setQuestion("Did meditate today?"); - h3.setDescription("test description 3"); - h3.setColor(new PaletteColor(10)); - habitList.update(h3); - - Habit h4 = fixtures.createEmptyHabit(); - h4.setName(EMPTY_DESCRIPTION_HABIT_NAME); - h4.setQuestion("Did you read books today?"); - h4.setDescription(""); - h4.setColor(new PaletteColor(2)); - habitList.update(h4); - } - - protected void rotateDevice() throws Exception - { - device.setOrientationLeft(); - device.setOrientationNatural(); - } - -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt new file mode 100644 index 000000000..720d998a0 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.linkedin.android.testbutler.TestButler +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.inject.HabitsApplicationComponent +import org.junit.After +import org.junit.Before + +open class BaseUserInterfaceTest { + private lateinit var component: HabitsApplicationComponent + private lateinit var habitList: HabitList + private lateinit var prefs: Preferences + private lateinit var fixtures: HabitFixtures + private lateinit var cache: HabitCardListCache + @Before + @Throws(Exception::class) + fun setUp() { + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + TestButler.setup(ApplicationProvider.getApplicationContext()) + TestButler.verifyAnimationsDisabled(ApplicationProvider.getApplicationContext()) + val app = + ApplicationProvider.getApplicationContext().applicationContext as HabitsApplication + component = app.component + habitList = component.habitList + prefs = component.preferences + cache = component.habitCardListCache + fixtures = HabitFixtures(component.modelFactory, habitList) + resetState() + } + + @After + @Throws(Exception::class) + fun tearDown() { + for (i in 0..9) device.pressBack() + TestButler.teardown(ApplicationProvider.getApplicationContext()) + } + + @Throws(Exception::class) + private fun resetState() { + prefs.clear() + prefs.isFirstRun = false + prefs.updateLastHint(100, getToday()) + habitList.removeAll() + cache.refreshAllHabits() + Thread.sleep(1000) + val h1 = fixtures.createEmptyHabit() + h1.name = "Wake up early" + h1.question = "Did you wake up early today?" + h1.description = "test description 1" + h1.color = PaletteColor(5) + habitList.update(h1) + val h2 = fixtures.createShortHabit() + h2.name = "Track time" + h2.question = "Did you track your time?" + h2.description = "test description 2" + h2.color = PaletteColor(5) + habitList.update(h2) + val h3 = fixtures.createLongHabit() + h3.name = "Meditate" + h3.question = "Did meditate today?" + h3.description = "test description 3" + h3.color = PaletteColor(10) + habitList.update(h3) + val h4 = fixtures.createEmptyHabit() + h4.name = EMPTY_DESCRIPTION_HABIT_NAME + h4.question = "Did you read books today?" + h4.description = "" + h4.color = PaletteColor(2) + habitList.update(h4) + } + + @Throws(Exception::class) + protected fun rotateDevice() { + device!!.setOrientationLeft() + device!!.setOrientationNatural() + } + + companion object { + private const val PKG = "org.isoron.uhabits" + const val EMPTY_DESCRIPTION_HABIT_NAME = "Read books" + lateinit var device: UiDevice + fun startActivity(cls: Class<*>) { + val intent = Intent() + intent.component = ComponentName(PKG, cls.canonicalName!!) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + ApplicationProvider.getApplicationContext().startActivity(intent) + } + } +} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/acceptance/steps/BackupSteps.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/acceptance/steps/BackupSteps.kt index 4e0a12ca2..6dea14697 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/acceptance/steps/BackupSteps.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/acceptance/steps/BackupSteps.kt @@ -20,7 +20,7 @@ package org.isoron.uhabits.acceptance.steps import androidx.test.uiautomator.UiSelector -import org.isoron.uhabits.BaseUserInterfaceTest.device +import org.isoron.uhabits.BaseUserInterfaceTest.Companion.device import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.SETTINGS import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu From f1c88797a3709f25580a3eed0a49009c62b21da6 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 23:42:04 +0100 Subject: [PATCH 34/35] Clean up code after conversions --- .../org/isoron/uhabits/BaseAndroidTest.kt | 9 ++- .../isoron/uhabits/BaseUserInterfaceTest.kt | 4 +- .../java/org/isoron/uhabits/BaseViewTest.kt | 12 ++-- .../isoron/uhabits/HabitsApplicationTest.kt | 6 +- .../activities/common/views/ScoreChartTest.kt | 27 ++++----- .../habits/list/views/HintViewTest.kt | 9 +-- .../habits/show/views/ScoreCardViewTest.kt | 10 +--- .../uhabits/database/AndroidDatabaseTest.kt | 13 ++--- .../uhabits/widgets/CheckmarkWidgetTest.kt | 14 +++-- .../activities/AndroidThemeSwitcher.kt | 4 -- .../activities/common/views/ScoreChart.kt | 4 +- .../common/views/ScrollableChart.kt | 58 +++++++++---------- .../habits/list/views/HabitCardView.kt | 16 +---- .../habits/list/views/HeaderView.kt | 4 +- .../habits/list/views/NumberButtonView.kt | 22 ++++--- .../habits/show/views/ScoreCardView.kt | 2 +- .../isoron/uhabits/tasks/AndroidTaskRunner.kt | 1 - .../org/isoron/uhabits/widgets/ScoreWidget.kt | 2 +- .../isoron/uhabits/core/models/HabitList.kt | 16 +++-- .../core/models/memory/MemoryHabitList.kt | 22 +++---- .../core/models/memory/package-info.java | 23 -------- .../core/models/sqlite/package-info.java | 23 -------- .../isoron/uhabits/core/test/HabitFixtures.kt | 2 +- .../screens/habits/list/ListHabitsBehavior.kt | 9 ++- .../list/ListHabitsSelectionMenuBehavior.kt | 16 +++-- .../uhabits/core/database/RepositoryTest.kt | 41 +++++++------ 26 files changed, 152 insertions(+), 217 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/package-info.java delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/package-info.java diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt index 76722b92e..ee7a1a332 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt @@ -27,8 +27,8 @@ import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import junit.framework.TestCase -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import org.hamcrest.CoreMatchers.hasItems +import org.hamcrest.MatcherAssert.assertThat import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.Timestamp @@ -109,9 +109,9 @@ abstract class BaseAndroidTest : TestCase() { val manager = AppWidgetManager.getInstance(targetContext) val installedProviders: MutableList = LinkedList() for (info in manager.installedProviders) installedProviders.add(info.provider) - MatcherAssert.assertThat>( + assertThat>( installedProviders, - CoreMatchers.hasItems(provider) + hasItems(provider) ) } @@ -208,7 +208,6 @@ abstract class BaseAndroidTest : TestCase() { @Throws(Exception::class) fun restoreSystemTime() { - if (savedCalendar == null) throw NullPointerException() setSystemTime(savedCalendar) } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt index 720d998a0..af7f503e3 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt @@ -99,8 +99,8 @@ open class BaseUserInterfaceTest { @Throws(Exception::class) protected fun rotateDevice() { - device!!.setOrientationLeft() - device!!.setOrientationNatural() + device.setOrientationLeft() + device.setOrientationNatural() } companion object { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt index c5cd6c94c..bcec69dfa 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt @@ -34,12 +34,10 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.Random +import kotlin.math.abs open class BaseViewTest : BaseAndroidTest() { var similarityCutoff = 0.00018 - override fun setUp() { - super.setUp() - } @Throws(IOException::class) protected fun assertRenders(view: View, expectedImagePath: String) { @@ -134,10 +132,10 @@ open class BaseViewTest : BaseAndroidTest() { if (random.nextInt(4) != 0) continue val argb1 = colorToArgb(b1.getPixel(x, y)) val argb2 = colorToArgb(b2.getPixel(x, y)) - distance += Math.abs(argb1[0] - argb2[0]).toDouble() - distance += Math.abs(argb1[1] - argb2[1]).toDouble() - distance += Math.abs(argb1[2] - argb2[2]).toDouble() - distance += Math.abs(argb1[3] - argb2[3]).toDouble() + distance += abs(argb1[0] - argb2[0]).toDouble() + distance += abs(argb1[1] - argb2[1]).toDouble() + distance += abs(argb1[2] - argb2[2]).toDouble() + distance += abs(argb1[3] - argb2[3]).toDouble() } } distance /= 255.0 * 16 * b1.width * b1.height diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt index f6996903f..4dfd13277 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.kt @@ -20,8 +20,8 @@ package org.isoron.uhabits import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.MatcherAssert.assertThat import org.junit.Test import org.junit.runner.RunWith import java.io.IOException @@ -35,6 +35,6 @@ class HabitsApplicationTest : BaseAndroidTest() { val msg = "LOGCAT TEST" RuntimeException(msg).printStackTrace() val log = AndroidBugReporter(targetContext).getLogcat() - MatcherAssert.assertThat(log, CoreMatchers.containsString(msg)) + assertThat(log, containsString(msg)) } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt index c4d1cbc87..bcd4a18dd 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt @@ -33,16 +33,17 @@ import org.junit.runner.RunWith class ScoreChartTest : BaseViewTest() { private lateinit var habit: Habit private lateinit var view: ScoreChart + @Before override fun setUp() { super.setUp() fixtures.purgeHabits(habitList) habit = fixtures.createLongHabit() - val (scores, bucketSize, _, color) = buildState(habit, prefs.firstWeekdayInt, 0) + val state = buildState(habit, prefs.firstWeekdayInt, 0) view = ScoreChart(targetContext).apply { - setScores(scores.toMutableList()) - setColor(color.toFixedAndroidColor()) - setBucketSize(bucketSize) + setScores(state.scores) + setColor(state.color.toFixedAndroidColor()) + setBucketSize(state.bucketSize) } measureView(view, dpToPixels(300), dpToPixels(200)) } @@ -71,12 +72,8 @@ class ScoreChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withMonthlyBucket() { - val (scores, bucketSize) = buildState( - habit, - prefs.firstWeekdayInt, - 2 - ) - view.setScores(scores.toMutableList()) + val (scores, bucketSize) = buildState(habit, prefs.firstWeekdayInt, 2) + view.setScores(scores) view.setBucketSize(bucketSize) view.invalidate() assertRenders(view, BASE_PATH + "renderMonthly.png") @@ -92,13 +89,9 @@ class ScoreChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withYearlyBucket() { - val (scores, bucketSize) = buildState( - habit, - prefs.firstWeekdayInt, - 4 - ) - view.setScores(scores.toMutableList()) - view.setBucketSize(bucketSize) + val state = buildState(habit, prefs.firstWeekdayInt, 4) + view.setScores(state.scores) + view.setBucketSize(state.bucketSize) view.invalidate() assertRenders(view, BASE_PATH + "renderYearly.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt index 1ff4104bd..af1f30df8 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.kt @@ -23,8 +23,8 @@ import androidx.test.filters.MediumTest import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.core.ui.screens.habits.list.HintList import org.junit.Before @@ -36,6 +36,7 @@ import org.junit.runner.RunWith class HintViewTest : BaseViewTest() { private lateinit var view: HintView private lateinit var list: HintList + @Before override fun setUp() { super.setUp() @@ -58,10 +59,10 @@ class HintViewTest : BaseViewTest() { @Test @Throws(Exception::class) fun testClick() { - MatcherAssert.assertThat(view.alpha, CoreMatchers.equalTo(1f)) + assertThat(view.alpha, equalTo(1f)) view.performClick() skipAnimation(view) - MatcherAssert.assertThat(view.alpha, CoreMatchers.equalTo(0f)) + assertThat(view.alpha, equalTo(0f)) } companion object { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardViewTest.kt index 481748ffd..b695c3a7b 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardViewTest.kt @@ -24,7 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R -import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter.Companion.buildState import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,13 +43,7 @@ class ScoreCardViewTest : BaseViewTest() { .from(targetContext) .inflate(R.layout.show_habit, null) .findViewById(R.id.scoreCard) as ScoreCardView - view.setState( - ScoreCardPresenter.buildState( - habit = habit, - firstWeekday = 0, - spinnerPosition = 0, - ) - ) + view.setState(buildState(habit = habit, firstWeekday = 0, spinnerPosition = 0)) measureView(view, 800f, 600f) } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt index 6fe668e3f..b58603789 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/database/AndroidDatabaseTest.kt @@ -19,12 +19,11 @@ package org.isoron.uhabits.database import android.database.sqlite.SQLiteDatabase -import org.hamcrest.MatcherAssert -import org.hamcrest.core.IsEqual +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo import org.isoron.uhabits.BaseAndroidTest import org.isoron.uhabits.core.database.Cursor import org.junit.Test -import java.util.HashMap class AndroidDatabaseTest : BaseAndroidTest() { private lateinit var db: AndroidDatabase @@ -37,13 +36,11 @@ class AndroidDatabaseTest : BaseAndroidTest() { @Test @Throws(Exception::class) fun testInsert() { - val map = HashMap() - map["name"] = "asd" - map["color"] = null + val map = mapOf(Pair("name", "asd"), Pair("color", null)) db.insert("test", map) val c: Cursor = db.query("select * from test") c.moveToNext() - assertNull(c.getInt(0)) - MatcherAssert.assertThat(c.getString(1), IsEqual.equalTo("asd")) + c.getInt(0)!! + assertThat(c.getString(1), equalTo("asd")) } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt index 0a5823bbf..b73c49c1b 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt @@ -24,7 +24,9 @@ import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Entry @@ -50,26 +52,26 @@ class CheckmarkWidgetTest : BaseViewTest() { entries = habit.computedEntries val widget = CheckmarkWidget(targetContext, 0, habit) view = convertToView(widget, 150, 200) - MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.YES_MANUAL)) + assertThat(entries.get(today).value, equalTo(Entry.YES_MANUAL)) } @Test @Throws(Exception::class) fun testClick() { val button = view.findViewById(R.id.button) as Button - MatcherAssert.assertThat( + assertThat( button, - CoreMatchers.`is`(CoreMatchers.not(CoreMatchers.nullValue())) + `is`(CoreMatchers.not(CoreMatchers.nullValue())) ) // A better test would be to capture the intent, but it doesn't seem // possible to capture intents sent to BroadcastReceivers. button.performClick() sleep(1000) - MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.SKIP)) + assertThat(entries.get(today).value, equalTo(Entry.SKIP)) button.performClick() sleep(1000) - MatcherAssert.assertThat(entries.get(today).value, CoreMatchers.equalTo(Entry.NO)) + assertThat(entries.get(today).value, equalTo(Entry.NO)) } @Test diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt index 262db7fda..855adaa84 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/AndroidThemeSwitcher.kt @@ -53,10 +53,6 @@ constructor( } } - // override fun getCurrentTheme(): Theme { - // return currentTheme - // } - override fun applyDarkTheme() { currentTheme = DarkTheme() context.setTheme(R.style.AppBaseThemeDark) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt index 286f20216..3a53f0f8a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt @@ -62,7 +62,7 @@ class ScoreChart : ScrollableChart { private var nColumns = 0 private var textColor = 0 private var gridColor = 0 - private var scores: MutableList? = null + private var scores: List? = null private var primaryColor = 0 @Deprecated("") @@ -113,7 +113,7 @@ class ScoreChart : ScrollableChart { postInvalidate() } - fun setScores(scores: MutableList) { + fun setScores(scores: List) { this.scores = scores postInvalidate() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt index 113b37076..509acc067 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt @@ -37,10 +37,10 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat private set private var scrollerBucketSize = 1 private var direction = 1 - private var detector: GestureDetector? = null - private var scroller: Scroller? = null - private var scrollAnimator: ValueAnimator? = null - private var scrollController: ScrollController? = null + private lateinit var detector: GestureDetector + private lateinit var scroller: Scroller + private lateinit var scrollAnimator: ValueAnimator + private lateinit var scrollController: ScrollController private var maxDataOffset = 10000 constructor(context: Context?) : super(context) { @@ -52,11 +52,11 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat } override fun onAnimationUpdate(animation: ValueAnimator) { - if (!scroller!!.isFinished) { - scroller!!.computeScrollOffset() + if (!scroller.isFinished) { + scroller.computeScrollOffset() updateDataOffset() } else { - scrollAnimator!!.cancel() + scrollAnimator.cancel() } } @@ -70,9 +70,9 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat velocityX: Float, velocityY: Float ): Boolean { - scroller!!.fling( - scroller!!.currX, - scroller!!.currY, + scroller.fling( + scroller.currX, + scroller.currY, direction * velocityX.toInt() / 2, 0, 0, @@ -81,13 +81,13 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat 0 ) invalidate() - scrollAnimator!!.duration = scroller!!.duration.toLong() - scrollAnimator!!.start() + scrollAnimator.duration = scroller.duration.toLong() + scrollAnimator.start() return false } private val maxX: Int - private get() = maxDataOffset * scrollerBucketSize + get() = maxDataOffset * scrollerBucketSize public override fun onRestoreInstanceState(state: Parcelable) { if (state !is BundleSavedState) { @@ -99,16 +99,16 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat direction = state.bundle.getInt("direction") dataOffset = state.bundle.getInt("dataOffset") maxDataOffset = state.bundle.getInt("maxDataOffset") - scroller!!.startScroll(0, 0, x, y, 0) - scroller!!.computeScrollOffset() + scroller.startScroll(0, 0, x, y, 0) + scroller.computeScrollOffset() super.onRestoreInstanceState(state.superState) } public override fun onSaveInstanceState(): Parcelable? { val superState = super.onSaveInstanceState() val bundle = Bundle().apply { - putInt("x", scroller!!.currX) - putInt("y", scroller!!.currY) + putInt("x", scroller.currX) + putInt("y", scroller.currY) putInt("dataOffset", dataOffset) putInt("direction", direction) putInt("maxDataOffset", maxDataOffset) @@ -124,15 +124,15 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat parent?.requestDisallowInterceptTouchEvent(true) } dx *= -direction - dx = min(dx, (maxX - scroller!!.currX).toFloat()) - scroller!!.startScroll( - scroller!!.currX, - scroller!!.currY, + dx = min(dx, (maxX - scroller.currX).toFloat()) + scroller.startScroll( + scroller.currX, + scroller.currY, dx.toInt(), dy.toInt(), 0 ) - scroller!!.computeScrollOffset() + scroller.computeScrollOffset() updateDataOffset() return true } @@ -143,7 +143,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat } override fun onTouchEvent(event: MotionEvent): Boolean { - return detector!!.onTouchEvent(event) + return detector.onTouchEvent(event) } fun setScrollDirection(direction: Int) { @@ -155,11 +155,11 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat fun setMaxDataOffset(maxDataOffset: Int) { this.maxDataOffset = maxDataOffset dataOffset = min(dataOffset, maxDataOffset) - scrollController!!.onDataOffsetChanged(dataOffset) + scrollController.onDataOffsetChanged(dataOffset) postInvalidate() } - fun setScrollController(scrollController: ScrollController?) { + fun setScrollController(scrollController: ScrollController) { this.scrollController = scrollController } @@ -177,18 +177,18 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat } fun reset() { - scroller!!.finalX = 0 - scroller!!.computeScrollOffset() + scroller.finalX = 0 + scroller.computeScrollOffset() updateDataOffset() } private fun updateDataOffset() { - var newDataOffset = scroller!!.currX / scrollerBucketSize + var newDataOffset = scroller.currX / scrollerBucketSize newDataOffset = max(0, newDataOffset) newDataOffset = min(maxDataOffset, newDataOffset) if (newDataOffset != dataOffset) { dataOffset = newDataOffset - scrollController!!.onDataOffsetChanged(dataOffset) + scrollController.onDataOffsetChanged(dataOffset) postInvalidate() } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index 26af7f3e9..21d81faaf 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -22,7 +22,6 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.M import android.os.Handler import android.os.Looper @@ -159,7 +158,7 @@ class HabitCardView( gravity = Gravity.CENTER_VERTICAL orientation = LinearLayout.HORIZONTAL layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) - if (SDK_INT >= LOLLIPOP) elevation = dp(1f) + elevation = dp(1f) addView(scoreRing) addView(label) @@ -167,8 +166,7 @@ class HabitCardView( addView(numberPanel) setOnTouchListener { v, event -> - if (SDK_INT >= LOLLIPOP) - v.background.setHotspot(event.x, event.y) + v.background.setHotspot(event.x, event.y) false } } @@ -247,7 +245,7 @@ class HabitCardView( private fun triggerRipple(x: Float, y: Float) { val background = innerFrame.background - if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y) + background.setHotspot(x, y) background.state = intArrayOf( android.R.attr.state_pressed, android.R.attr.state_enabled @@ -256,14 +254,6 @@ class HabitCardView( } private fun updateBackground(isSelected: Boolean) { - if (SDK_INT < LOLLIPOP) { - val background = when (isSelected) { - true -> sres.getDrawable(R.attr.selectedBackground) - false -> sres.getDrawable(R.attr.cardBackground) - } - innerFrame.setBackgroundDrawable(background) - return - } val background = when (isSelected) { true -> R.drawable.selected_box diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt index 2c46bb2b3..fc7edbba8 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt @@ -25,8 +25,6 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.graphics.Typeface -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.text.TextPaint import android.view.View.MeasureSpec.EXACTLY import org.isoron.uhabits.R @@ -60,7 +58,7 @@ class HeaderView( init { setScrollerBucketSize(dim(R.dimen.checkmarkWidth).toInt()) setBackgroundColor(sres.getColor(R.attr.headerBackgroundColor)) - if (SDK_INT >= LOLLIPOP) elevation = dp(2.0f) + elevation = dp(2.0f) } override fun atMidnight() { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt index 2b38bf9e9..41f72c2b4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt @@ -161,15 +161,19 @@ class NumberButtonView( val label: String val typeface: Typeface - if (value >= 0) { - label = value.toShortString() - typeface = BOLD_TYPEFACE - } else if (preferences.areQuestionMarksEnabled()) { - label = resources.getString(R.string.fa_question) - typeface = getFontAwesome() - } else { - label = "0" - typeface = BOLD_TYPEFACE + when { + value >= 0 -> { + label = value.toShortString() + typeface = BOLD_TYPEFACE + } + preferences.areQuestionMarksEnabled() -> { + label = resources.getString(R.string.fa_question) + typeface = getFontAwesome() + } + else -> { + label = "0" + typeface = BOLD_TYPEFACE + } } pBold.color = activeColor diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt index fb28bc055..e5c740574 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt @@ -36,7 +36,7 @@ class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex val androidColor = state.color.toThemedAndroidColor(context) binding.title.setTextColor(androidColor) binding.spinner.setSelection(state.spinnerPosition) - binding.scoreView.setScores(state.scores.toMutableList()) + binding.scoreView.setScores(state.scores) binding.scoreView.reset() binding.scoreView.setBucketSize(state.bucketSize) binding.scoreView.setColor(androidColor) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt index f2f519ca0..aa96604c2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt @@ -75,7 +75,6 @@ class AndroidTaskRunner : TaskRunner { } override fun onPreExecute() { - // isCancelled = task.isCanceled if (isCancelled) return for (l in listeners) l.onTaskStarted(task) activeTasks.add(this) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt index 45d78e9f2..4beeed281 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt @@ -52,7 +52,7 @@ class ScoreWidget( setIsTransparencyEnabled(true) setBucketSize(viewModel.bucketSize) setColor(habit.color.toThemedAndroidColor(context)) - setScores(viewModel.scores.toMutableList()) + setScores(viewModel.scores) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 571360e19..69b738dda 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -30,13 +30,13 @@ import javax.annotation.concurrent.ThreadSafe @ThreadSafe abstract class HabitList : Iterable { val observable: ModelObservable + @JvmField protected val filter: HabitMatcher /** * Creates a new HabitList. * - * * Depending on the implementation, this list can either be empty or be * populated by some pre-existing habits, for example, from a certain * database. @@ -54,7 +54,6 @@ abstract class HabitList : Iterable { /** * Inserts a new habit in the list. * - * * If the id of the habit is null, the list will assign it a new id, which * is guaranteed to be unique in the scope of the list. If id is not null, * the caller should make sure that the list does not already contain @@ -115,7 +114,6 @@ abstract class HabitList : Iterable { /** * Removes the given habit from the list. * - * * If the given habit is not in the list, does nothing. * * @param h the habit to be removed. @@ -151,7 +149,6 @@ abstract class HabitList : Iterable { /** * Notifies the list that a certain list of habits has been modified. * - * * Depending on the implementation, this operation might trigger a write to * disk, or do nothing at all. To make sure that the habits get persisted, * this operation must be called. @@ -163,7 +160,6 @@ abstract class HabitList : Iterable { /** * Notifies the list that a certain habit has been modified. * - * * See [.update] for more details. * * @param habit the habit that has been modified. @@ -212,6 +208,14 @@ abstract class HabitList : Iterable { abstract fun resort() enum class Order { - BY_NAME_ASC, BY_NAME_DESC, BY_COLOR_ASC, BY_COLOR_DESC, BY_SCORE_ASC, BY_SCORE_DESC, BY_STATUS_ASC, BY_STATUS_DESC, BY_POSITION + BY_NAME_ASC, + BY_NAME_DESC, + BY_COLOR_ASC, + BY_COLOR_DESC, + BY_SCORE_ASC, + BY_SCORE_DESC, + BY_STATUS_ASC, + BY_STATUS_DESC, + BY_POSITION } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt index 129d4971a..6e6bace1a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -151,16 +151,18 @@ class MemoryHabitList : HabitList { } val statusComparatorAsc = Comparator { h1: Habit, h2: Habit -> statusComparatorDesc.compare(h2, h1) } - if (order === Order.BY_POSITION) return positionComparator - if (order === Order.BY_NAME_ASC) return nameComparatorAsc - if (order === Order.BY_NAME_DESC) return nameComparatorDesc - if (order === Order.BY_COLOR_ASC) return colorComparatorAsc - if (order === Order.BY_COLOR_DESC) return colorComparatorDesc - if (order === Order.BY_SCORE_DESC) return scoreComparatorDesc - if (order === Order.BY_SCORE_ASC) return scoreComparatorAsc - if (order === Order.BY_STATUS_DESC) return statusComparatorDesc - if (order === Order.BY_STATUS_ASC) return statusComparatorAsc - throw IllegalStateException() + return when { + order === Order.BY_POSITION -> positionComparator + order === Order.BY_NAME_ASC -> nameComparatorAsc + order === Order.BY_NAME_DESC -> nameComparatorDesc + order === Order.BY_COLOR_ASC -> colorComparatorAsc + order === Order.BY_COLOR_DESC -> colorComparatorDesc + order === Order.BY_SCORE_DESC -> scoreComparatorDesc + order === Order.BY_SCORE_ASC -> scoreComparatorAsc + order === Order.BY_STATUS_DESC -> statusComparatorDesc + order === Order.BY_STATUS_ASC -> statusComparatorAsc + else -> throw IllegalStateException() + } } @Synchronized diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/package-info.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/package-info.java deleted file mode 100644 index 53f4766ab..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -/** - * Provides in-memory implementation of core models. - */ -package org.isoron.uhabits.core.models.memory; \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/package-info.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/package-info.java deleted file mode 100644 index bc872132d..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -/** - * Provides SQLite implementations of the core models. - */ -package org.isoron.uhabits.core.models.sqlite; \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt index 86eaa122e..fca65af71 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt @@ -29,7 +29,7 @@ import org.isoron.uhabits.core.models.sqlite.SQLiteEntryList import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday class HabitFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList) { - var NON_DAILY_HABIT_CHECKS = booleanArrayOf( + private var NON_DAILY_HABIT_CHECKS = booleanArrayOf( true, false, false, true, true, true, false, false, true, true ) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 515ec6343..9877e8ad4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -123,7 +123,14 @@ open class ListHabitsBehavior @Inject constructor( } enum class Message { - COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED, SYNC_KEY_ALREADY_INSTALLED + COULD_NOT_EXPORT, + IMPORT_SUCCESSFUL, + IMPORT_FAILED, + DATABASE_REPAIRED, + COULD_NOT_GENERATE_BUG_REPORT, + FILE_NOT_RECOGNIZED, + SYNC_ENABLED, + SYNC_KEY_ALREADY_INSTALLED } interface BugReporter { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index ebd033480..cc43ecba4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -37,7 +37,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( var commandRunner: CommandRunner ) { fun canArchive(): Boolean { - for ((_, _, _, _, isArchived) in adapter.selected) if (isArchived) return false + for (habit in adapter.selected) if (habit.isArchived) return false return true } @@ -46,7 +46,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( } fun canUnarchive(): Boolean { - for ((_, _, _, _, isArchived) in adapter.selected) if (!isArchived) return false + for (habit in adapter.selected) if (!habit.isArchived) return false return true } @@ -56,23 +56,21 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( } fun onChangeColor() { - val selected = adapter.selected - val (color) = selected[0] + val (color) = adapter.selected[0] screen.showColorPicker(color) { selectedColor: PaletteColor -> - commandRunner.run(ChangeHabitColorCommand(habitList, selected, selectedColor)) + commandRunner.run(ChangeHabitColorCommand(habitList, adapter.selected, selectedColor)) adapter.clearSelection() } } fun onDeleteHabits() { - val selected = adapter.selected screen.showDeleteConfirmationScreen( { - adapter.performRemove(selected) - commandRunner.run(DeleteHabitsCommand(habitList, selected)) + adapter.performRemove(adapter.selected) + commandRunner.run(DeleteHabitsCommand(habitList, adapter.selected)) adapter.clearSelection() }, - selected.size + adapter.selected.size ) } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt index c07e0bf9a..30c79d700 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/database/RepositoryTest.kt @@ -18,19 +18,20 @@ */ package org.isoron.uhabits.core.database +import junit.framework.Assert.assertNull import org.apache.commons.lang3.builder.EqualsBuilder import org.apache.commons.lang3.builder.HashCodeBuilder import org.apache.commons.lang3.builder.ToStringBuilder -import org.hamcrest.MatcherAssert -import org.hamcrest.core.IsEqual +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo import org.isoron.uhabits.core.BaseUnitTest -import org.junit.Assert import org.junit.Before import org.junit.Test class RepositoryTest : BaseUnitTest() { private lateinit var repository: Repository private lateinit var db: Database + @Before @Throws(Exception::class) override fun setUp() { @@ -54,11 +55,10 @@ class RepositoryTest : BaseUnitTest() { "values (10, 20, 'hello', 8.0)" ) val record = repository.find(10L) - Assert.assertNotNull(record) - MatcherAssert.assertThat(record!!.id, IsEqual.equalTo(10L)) - MatcherAssert.assertThat(record.color, IsEqual.equalTo(20)) - MatcherAssert.assertThat(record.name, IsEqual.equalTo("hello")) - MatcherAssert.assertThat(record.score, IsEqual.equalTo(8.0)) + assertThat(record!!.id, equalTo(10L)) + assertThat(record.color, equalTo(20)) + assertThat(record.name, equalTo("hello")) + assertThat(record.score, equalTo(8.0)) } @Test @@ -71,11 +71,11 @@ class RepositoryTest : BaseUnitTest() { score = 5.0 } repository.save(record) - MatcherAssert.assertThat(record, IsEqual.equalTo(repository.find(50L))) + assertThat(record, equalTo(repository.find(50L))) record.name = "world" record.score = 128.0 repository.save(record) - MatcherAssert.assertThat(record, IsEqual.equalTo(repository.find(50L))) + assertThat(record, equalTo(repository.find(50L))) } @Test @@ -88,9 +88,8 @@ class RepositoryTest : BaseUnitTest() { } repository.save(record) val retrieved = repository.find(record.id!!) - Assert.assertNotNull(retrieved) - Assert.assertNull(retrieved!!.name) - MatcherAssert.assertThat(record, IsEqual.equalTo(retrieved)) + assertNull(retrieved!!.name) + assertThat(record, equalTo(retrieved)) } @Test @@ -108,8 +107,8 @@ class RepositoryTest : BaseUnitTest() { score = 2.0 } repository.save(r2) - MatcherAssert.assertThat(r1.id, IsEqual.equalTo(1L)) - MatcherAssert.assertThat(r2.id, IsEqual.equalTo(2L)) + assertThat(r1.id, equalTo(1L)) + assertThat(r2.id, equalTo(2L)) } @Test @@ -128,14 +127,14 @@ class RepositoryTest : BaseUnitTest() { } repository.save(rec2) val id = rec1.id!! - MatcherAssert.assertThat(rec1, IsEqual.equalTo(repository.find(id))) - MatcherAssert.assertThat(rec2, IsEqual.equalTo(repository.find(rec2.id!!))) + assertThat(rec1, equalTo(repository.find(id))) + assertThat(rec2, equalTo(repository.find(rec2.id!!))) repository.remove(rec1) - MatcherAssert.assertThat(rec1.id, IsEqual.equalTo(null)) - Assert.assertNull(repository.find(id)) - MatcherAssert.assertThat(rec2, IsEqual.equalTo(repository.find(rec2.id!!))) + assertThat(rec1.id, equalTo(null)) + assertNull(repository.find(id)) + assertThat(rec2, equalTo(repository.find(rec2.id!!))) repository.remove(rec1) // should have no effect - Assert.assertNull(repository.find(id)) + assertNull(repository.find(id)) } @Table(name = "tests") From 14d018c327d8215a1dd9de659563c09881fcc1dc Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Mon, 25 Jan 2021 10:16:07 +0100 Subject: [PATCH 35/35] Convert Timestamp to data class --- .../isoron/uhabits/core/models/Timestamp.kt | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt index 544d31eb1..feb118ce5 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt @@ -18,8 +18,6 @@ */ package org.isoron.uhabits.core.models -import org.apache.commons.lang3.builder.EqualsBuilder -import org.apache.commons.lang3.builder.HashCodeBuilder import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat import org.isoron.uhabits.core.utils.DateUtils @@ -30,8 +28,7 @@ import java.util.Date import java.util.GregorianCalendar import java.util.TimeZone -class Timestamp(unixTime: Long) : Comparable { - val unixTime: Long +data class Timestamp(var unixTime: Long) : Comparable { constructor(cal: GregorianCalendar) : this(cal.timeInMillis) @@ -49,19 +46,6 @@ class Timestamp(unixTime: Long) : Comparable { return java.lang.Long.signum(unixTime - other.unixTime) } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val timestamp = other as Timestamp - return EqualsBuilder() - .append(unixTime, timestamp.unixTime) - .isEquals - } - - override fun hashCode(): Int { - return HashCodeBuilder(17, 37).append(unixTime).toHashCode() - } - operator fun minus(days: Int): Timestamp { return plus(-days) } @@ -140,9 +124,7 @@ class Timestamp(unixTime: Long) : Comparable { } init { - var unixTime = unixTime require(unixTime >= 0) { "Invalid unix time: $unixTime" } if (unixTime % DAY_LENGTH != 0L) unixTime = unixTime / DAY_LENGTH * DAY_LENGTH - this.unixTime = unixTime } }