Convert most of code remaining in uhabits-core

pull/716/head
Quentin Hibon 5 years ago
parent e84cc8e8b1
commit 136ec5b49b

@ -215,7 +215,7 @@ class ListHabitsScreen
) )
} }
override fun showSendBugReportToDeveloperScreen(log: String) { override fun showSendBugReportToDeveloperScreen(log: String?) {
val to = R.string.bugReportTo val to = R.string.bugReportTo
val subject = R.string.bugReportSubject val subject = R.string.bugReportSubject
activity.showSendEmailScreen(to, subject, log) activity.showSendEmailScreen(to, subject, log)
@ -230,10 +230,7 @@ class ListHabitsScreen
activity.startActivityForResult(intent, REQUEST_SETTINGS) activity.startActivityForResult(intent, REQUEST_SETTINGS)
} }
override fun showColorPicker( override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
defaultColor: PaletteColor,
callback: OnColorPickedCallback
) {
val picker = colorPickerFactory.create(defaultColor) val picker = colorPickerFactory.create(defaultColor)
picker.setListener(callback) picker.setListener(callback)
picker.show(activity.supportFragmentManager, "picker") picker.show(activity.supportFragmentManager, "picker")

@ -53,7 +53,7 @@ class HabitCardListAdapter @Inject constructor(
ListHabitsSelectionMenuBehavior.Adapter { ListHabitsSelectionMenuBehavior.Adapter {
val observable: ModelObservable = ModelObservable() val observable: ModelObservable = ModelObservable()
private var listView: HabitCardListView? = null private var listView: HabitCardListView? = null
private val selected: LinkedList<Habit> = LinkedList() override val selected: LinkedList<Habit> = LinkedList()
override fun atMidnight() { override fun atMidnight() {
cache.refreshAllHabits() cache.refreshAllHabits()
} }
@ -90,9 +90,9 @@ class HabitCardListAdapter @Inject constructor(
return getItem(position)!!.id!! return getItem(position)!!.id!!
} }
override fun getSelected(): List<Habit> { // override fun getSelected(): List<Habit> {
return LinkedList(selected) // return LinkedList(selected)
} // }
/** /**
* Returns whether list of selected items is empty. * 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 * database operation to finish, the cache can be modified to reflect the
* changes immediately. * changes immediately.
* *
* @param habits list of habits to be removed * @param selected list of habits to be removed
*/ */
override fun performRemove(habits: List<Habit>) { override fun performRemove(selected: List<Habit>) {
for (habit in habits) cache.remove(habit.id!!) for (habit in selected) cache.remove(habit.id!!)
} }
/** /**
@ -209,8 +209,8 @@ class HabitCardListAdapter @Inject constructor(
cache.refreshAllHabits() cache.refreshAllHabits()
} }
override fun setFilter(matcher: HabitMatcher) { override fun setFilter(matcher: HabitMatcher?) {
cache.setFilter(matcher) if (matcher != null) cache.setFilter(matcher)
} }
/** /**
@ -225,19 +225,19 @@ class HabitCardListAdapter @Inject constructor(
this.listView = listView this.listView = listView
} }
override fun setPrimaryOrder(order: HabitList.Order) { override var primaryOrder: HabitList.Order
cache.primaryOrder = order get() = cache.primaryOrder
preferences.defaultPrimaryOrder = order set(value) {
} cache.primaryOrder = value
preferences.defaultPrimaryOrder = value
}
override fun setSecondaryOrder(order: HabitList.Order) { override var secondaryOrder: HabitList.Order
cache.secondaryOrder = order get() = cache.secondaryOrder
preferences.defaultSecondaryOrder = order set(value) {
} cache.secondaryOrder = value
preferences.defaultSecondaryOrder = value
override fun getPrimaryOrder(): HabitList.Order { }
return cache.primaryOrder
}
/** /**
* Selects or deselects the item at a given position. * Selects or deselects the item at a given position.

@ -117,8 +117,6 @@ class CheckmarkWidgetView : HabitWidgetView {
get() = R.layout.widget_checkmark get() = R.layout.widget_checkmark
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec = widthMeasureSpec
var heightMeasureSpec = heightMeasureSpec
val width = MeasureSpec.getSize(widthMeasureSpec) val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec)
var w = width.toFloat() var w = width.toFloat()
@ -128,15 +126,15 @@ class CheckmarkWidgetView : HabitWidgetView {
h *= scale h *= scale
if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility = if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility =
GONE else ring.visibility = VISIBLE GONE else ring.visibility = VISIBLE
widthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY) val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY)
heightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY) val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY)
var textSize = 0.15f * h var textSize = 0.15f * h
val maxTextSize = getDimension(context, R.dimen.smallerTextSize) val maxTextSize = getDimension(context, R.dimen.smallerTextSize)
textSize = min(textSize, maxTextSize) textSize = min(textSize, maxTextSize)
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
ring.setTextSize(textSize) ring.setTextSize(textSize)
ring.setThickness(0.15f * textSize) ring.setThickness(0.15f * textSize)
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec)
} }
private fun init() { private fun init() {

@ -77,7 +77,7 @@ class HabitsCSVExporter(
} }
private fun sanitizeFilename(name: String): String { 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)) return s.substring(0, min(s.length, 100))
} }

@ -219,7 +219,7 @@ public abstract class HabitList implements Iterable<Habit>
*/ */
public void writeCSV(@NonNull Writer out) throws IOException public void writeCSV(@NonNull Writer out) throws IOException
{ {
String header[] = { String[] header = {
"Position", "Position",
"Name", "Name",
"Question", "Question",

@ -23,6 +23,7 @@ import androidx.annotation.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
@ -96,7 +97,7 @@ public class MemoryHabitList extends HabitList
@Override @Override
public synchronized Habit getByUUID(String uuid) 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; return null;
} }
@ -114,12 +115,14 @@ public class MemoryHabitList extends HabitList
return new MemoryHabitList(matcher, comparator, this); return new MemoryHabitList(matcher, comparator, this);
} }
@NotNull
@Override @Override
public synchronized Order getPrimaryOrder() public synchronized Order getPrimaryOrder()
{ {
return primaryOrder; return primaryOrder;
} }
@NotNull
@Override @Override
public synchronized Order getSecondaryOrder() public synchronized Order getSecondaryOrder()
{ {
@ -288,7 +291,7 @@ public class MemoryHabitList extends HabitList
public synchronized void resort() public synchronized void resort()
{ {
if (comparator != null) Collections.sort(list, comparator); if (comparator != null) list.sort(comparator);
getObservable().notifyListeners(); getObservable().notifyListeners();
} }
} }

@ -25,6 +25,7 @@ import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*; import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.core.models.sqlite.records.*; import org.isoron.uhabits.core.models.sqlite.records.*;
import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
@ -161,6 +162,7 @@ public class SQLiteHabitList extends HabitList
return list.indexOf(h); return list.indexOf(h);
} }
@NotNull
@Override @Override
public synchronized Iterator<Habit> iterator() public synchronized Iterator<Habit> iterator()
{ {

@ -22,6 +22,8 @@ package org.isoron.uhabits.core.models.sqlite.records;
import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import java.util.Objects;
/** /**
* The SQLite database record corresponding to a {@link Habit}. * The SQLite database record corresponding to a {@link Habit}.
*/ */
@ -108,7 +110,7 @@ public class HabitRecord
if (model.hasReminder()) if (model.hasReminder())
{ {
Reminder reminder = model.getReminder(); Reminder reminder = model.getReminder();
this.reminderHour = reminder.getHour(); this.reminderHour = Objects.requireNonNull(reminder).getHour();
this.reminderMin = reminder.getMinute(); this.reminderMin = reminder.getMinute();
this.reminderDays = reminder.getDays().toInteger(); this.reminderDays = reminder.getDays().toInteger();
} }

@ -20,7 +20,6 @@
package org.isoron.uhabits.core.preferences; package org.isoron.uhabits.core.preferences;
import org.isoron.uhabits.core.AppScope; import org.isoron.uhabits.core.AppScope;
import org.isoron.uhabits.core.models.HabitNotFoundException;
import javax.inject.Inject; import javax.inject.Inject;
@ -33,7 +32,7 @@ public class WidgetPreferences {
this.storage = storage; this.storage = storage;
} }
public void addWidget(int widgetId, long habitIds[]) { public void addWidget(int widgetId, long[] habitIds) {
storage.putLongArray(getHabitIdKey(widgetId), habitIds); storage.putLongArray(getHabitIdKey(widgetId), habitIds);
} }

@ -77,7 +77,7 @@ public class ReminderScheduler implements CommandRunner.Listener
return; return;
} }
long reminderTime = habit.getReminder().getTimeInMillis(); long reminderTime = Objects.requireNonNull(habit.getReminder()).getTimeInMillis();
long snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.getId()); long snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.getId());
if (snoozeReminderTime != 0) if (snoozeReminderTime != 0)

@ -145,7 +145,7 @@ public class NotificationTray
void log(String msg); void log(String msg);
} }
class NotificationData static class NotificationData
{ {
public final Timestamp timestamp; public final Timestamp timestamp;
@ -229,7 +229,7 @@ public class NotificationTray
if (!habit.hasReminder()) return false; if (!habit.hasReminder()) return false;
Reminder reminder = habit.getReminder(); Reminder reminder = habit.getReminder();
boolean reminderDays[] = reminder.getDays().toArray(); boolean[] reminderDays = Objects.requireNonNull(reminder).getDays().toArray();
int weekday = timestamp.getWeekday(); int weekday = timestamp.getWeekday();
return reminderDays[weekday]; return reminderDays[weekday];

@ -121,8 +121,7 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
override fun onCommandFinished(command: Command) { override fun onCommandFinished(command: Command) {
if (command is CreateRepetitionCommand) { if (command is CreateRepetitionCommand) {
val (_, _, _, id) = command.habit command.habit.id?.let { refreshHabit(it) }
id?.let { refreshHabit(it) }
} else { } else {
refreshAllHabits() refreshAllHabits()
} }

@ -16,54 +16,31 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.core.ui.screens.habits.list
package org.isoron.uhabits.core.ui.screens.habits.list; import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
import androidx.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
/** /**
* Provides a list of hints to be shown at the application startup, and takes * Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown. * care of deciding when a new hint should be shown.
*/ */
public class HintList class HintList(private val prefs: Preferences, private val hints: Array<String>) {
{
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;
}
/** /**
* Returns a new hint to be shown to the user. * Returns a new hint to be shown to the user.
* <p> *
*
* The hint returned is marked as read on the list, and will not be returned * 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 * again. In case all hints have already been read, and there is nothing
* left, returns null. * left, returns null.
* *
* @return the next hint to be shown, or null if none * @return the next hint to be shown, or null if none
*/ */
public String pop() fun pop(): String? {
{ val next = prefs.lastHintNumber + 1
int next = prefs.getLastHintNumber() + 1; if (next >= hints.size) return null
if (next >= hints.length) return null; prefs.updateLastHint(next, getToday())
return hints[next]
prefs.updateLastHint(next, DateUtils.getToday());
return hints[next];
} }
/** /**
@ -71,10 +48,9 @@ public class HintList
* *
* @return true if hint should be shown, false otherwise * @return true if hint should be shown, false otherwise
*/ */
public boolean shouldShow() fun shouldShow(): Boolean {
{ val today = getToday()
Timestamp today = DateUtils.getToday(); val lastHintTimestamp = prefs.lastHintTimestamp
Timestamp lastHintTimestamp = prefs.getLastHintTimestamp(); return lastHintTimestamp.isOlderThan(today)
return (lastHintTimestamp.isOlderThan(today));
} }
} }

@ -1,216 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Habit> 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);
}
}

@ -0,0 +1,161 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Habit> = 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)
}
}

@ -1,177 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

@ -0,0 +1,133 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

@ -1,145 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Habit> 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<Habit> 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<Habit> getSelected();
void performRemove(List<Habit> selected);
}
public interface Screen
{
void showColorPicker(PaletteColor defaultColor,
@NonNull OnColorPickedCallback callback);
void showDeleteConfirmationScreen(
@NonNull OnConfirmedCallback callback,
int quantity);
void showEditHabitsScreen(@NonNull List<Habit> selected);
}
}

@ -0,0 +1,116 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Habit>
fun performRemove(selected: List<Habit>)
}
interface Screen {
fun showColorPicker(
defaultColor: PaletteColor,
callback: OnColorPickedCallback
)
fun showDeleteConfirmationScreen(
callback: OnConfirmedCallback,
quantity: Int
)
fun showEditHabitsScreen(selected: List<Habit>)
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.ui.screens.habits.list package org.isoron.uhabits.core.ui.screens.habits.list
import com.nhaarman.mockitokotlin2.KArgumentCaptor import com.nhaarman.mockitokotlin2.KArgumentCaptor
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.clearInvocations import com.nhaarman.mockitokotlin2.clearInvocations
import com.nhaarman.mockitokotlin2.eq 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.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
import java.io.IOException import java.io.IOException
import java.nio.file.Files import java.nio.file.Files
@ -89,9 +89,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Throws(Exception::class) @Throws(Exception::class)
fun testOnExportCSV() { fun testOnExportCSV() {
val outputDir = Files.createTempDirectory("CSV").toFile() val outputDir = Files.createTempDirectory("CSV").toFile()
whenever(dirFinder.csvOutputDir).thenReturn(outputDir) whenever(dirFinder.getCSVOutputDir()).thenReturn(outputDir)
behavior.onExportCSV() behavior.onExportCSV()
verify(screen).showSendFileScreen(ArgumentMatchers.any()) verify(screen).showSendFileScreen(any())
assertThat(FileUtils.listFiles(outputDir, null, false).size, equalTo(1)) assertThat(FileUtils.listFiles(outputDir, null, false).size, equalTo(1))
FileUtils.deleteDirectory(outputDir) FileUtils.deleteDirectory(outputDir)
} }
@ -101,7 +101,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
fun testOnExportCSV_fail() { fun testOnExportCSV_fail() {
val outputDir = Files.createTempDirectory("CSV").toFile() val outputDir = Files.createTempDirectory("CSV").toFile()
outputDir.setWritable(false) outputDir.setWritable(false)
whenever(dirFinder.csvOutputDir).thenReturn(outputDir) whenever(dirFinder.getCSVOutputDir()).thenReturn(outputDir)
behavior.onExportCSV() behavior.onExportCSV()
verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_EXPORT) verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_EXPORT)
assertTrue(outputDir.delete()) assertTrue(outputDir.delete())
@ -131,11 +131,11 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun testOnSendBugReport() { fun testOnSendBugReport() {
whenever(bugReporter.bugReport).thenReturn("hello") whenever(bugReporter.getBugReport()).thenReturn("hello")
behavior.onSendBugReport() behavior.onSendBugReport()
verify(bugReporter).dumpBugReportToFile() verify(bugReporter).dumpBugReportToFile()
verify(screen).showSendBugReportToDeveloperScreen("hello") verify(screen).showSendBugReportToDeveloperScreen("hello")
whenever(bugReporter.bugReport).thenThrow(IOException()) whenever(bugReporter.getBugReport()).thenThrow(IOException())
behavior.onSendBugReport() behavior.onSendBugReport()
verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_GENERATE_BUG_REPORT) verify(screen).showMessage(ListHabitsBehavior.Message.COULD_NOT_GENERATE_BUG_REPORT)
} }

@ -36,7 +36,6 @@ import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
class ListHabitsMenuBehaviorTest : BaseUnitTest() { class ListHabitsMenuBehaviorTest : BaseUnitTest() {
private lateinit var behavior: ListHabitsMenuBehavior private lateinit var behavior: ListHabitsMenuBehavior
@ -116,7 +115,7 @@ class ListHabitsMenuBehaviorTest : BaseUnitTest() {
whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_NAME_ASC) whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_NAME_ASC)
behavior.onSortByStatus() behavior.onSortByStatus()
verify(adapter).primaryOrder = orderCaptor.capture() 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(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_ASC))
assertThat(secondaryOrderCaptor.lastValue, equalTo(HabitList.Order.BY_NAME_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) whenever(adapter.primaryOrder).thenReturn(HabitList.Order.BY_STATUS_ASC)
behavior.onSortByStatus() behavior.onSortByStatus()
verify(adapter).primaryOrder = orderCaptor.capture() verify(adapter).primaryOrder = orderCaptor.capture()
verify(adapter, never()).setSecondaryOrder(ArgumentMatchers.any()) verify(adapter, never()).secondaryOrder
assertThat(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_DESC)) assertThat(orderCaptor.lastValue, equalTo(HabitList.Order.BY_STATUS_DESC))
} }

Loading…
Cancel
Save