From 1fcfb9b22edc8f185195c3d6c86c86a09a2f7cc2 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 13:41:45 -0400 Subject: [PATCH 001/187] First version of sync feature --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 + .../java/org/isoron/uhabits/BaseActivity.java | 126 +++++++++------ .../java/org/isoron/uhabits/MainActivity.java | 54 +------ .../java/org/isoron/uhabits/SyncManager.java | 151 ++++++++++++++++++ .../org/isoron/uhabits/commands/Command.java | 25 +++ .../commands/ToggleRepetitionCommand.java | 57 ++++++- .../uhabits/helpers/DatabaseHelper.java | 7 + 8 files changed, 317 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/SyncManager.java diff --git a/app/build.gradle b/app/build.gradle index 35eaaa0dc..c7dc17682 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { compile 'com.github.paolorotolo:appintro:3.4.0' compile 'org.apmem.tools:layouts:1.10@aar' compile 'com.opencsv:opencsv:3.7' + compile 'com.github.nkzawa:socket.io-client:0.3.0' compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3bac419f5..a19760a1d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,8 @@ + + undoList; - private LinkedList redoList; private Toast toast; + private SyncManager sync; Thread.UncaughtExceptionHandler androidExceptionHandler; @@ -57,38 +64,7 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); - undoList = new LinkedList<>(); - redoList = new LinkedList<>(); - } - - public void executeCommand(Command command, Long refreshKey) - { - executeCommand(command, false, refreshKey); - } - - protected void undo() - { - if (undoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_undo); - return; - } - - Command last = undoList.pop(); - redoList.push(last); - last.undo(); - showToast(last.getUndoStringId()); - } - - protected void redo() - { - if (redoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_redo); - return; - } - Command last = redoList.pop(); - executeCommand(last, false, null); + sync = new SyncManager(this); } public void showToast(Integer stringId) @@ -99,27 +75,29 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U toast.show(); } - public void executeCommand(final Command command, Boolean clearRedoStack, final Long refreshKey) + public void executeCommand(final Command command, final Long refreshKey) { - undoList.push(command); - - if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast(); - if (clearRedoStack) redoList.clear(); + executeCommand(command, refreshKey, true); + } - new AsyncTask() + public void executeCommand(final Command command, final Long refreshKey, + final boolean shouldBroadcast) + { + new BaseTask() { @Override - protected Void doInBackground(Void... params) + protected void doInBackground() { + Log.d("BaseActivity", "Executing command"); command.execute(); - return null; } @Override protected void onPostExecute(Void aVoid) { - BaseActivity.this.onPostExecuteCommand(refreshKey); + BaseActivity.this.onPostExecuteCommand(command, refreshKey); BackupManager.dataChanged("org.isoron.uhabits"); + if(shouldBroadcast) sync.postCommand(command); } }.execute(); @@ -127,6 +105,19 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U showToast(command.getExecuteStringId()); } + public void onPostExecuteCommand(Command command, Long refreshKey) + { + new BaseTask() + { + @Override + protected void doInBackground() + { + dismissNotifications(BaseActivity.this); + updateWidgets(BaseActivity.this); + } + }.execute(); + } + protected void setupSupportActionBar(boolean homeButtonEnabled) { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -144,10 +135,6 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U actionBar.setDisplayHomeAsUpEnabled(true); } - public void onPostExecuteCommand(Long refreshKey) - { - } - @Override public void uncaughtException(Thread thread, Throwable ex) { @@ -201,4 +188,39 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U view = findViewById(R.id.headerShadow); if(view != null) view.setVisibility(View.GONE); } + + @Override + protected void onDestroy() + { + sync.close(); + super.onDestroy(); + } + + private void dismissNotifications(Context context) + { + for(Habit h : Habit.getHabitsWithReminder()) + { + if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED) + HabitBroadcastReceiver.dismissNotification(context, h); + } + } + + public static void updateWidgets(Context context) + { + updateWidgets(context, CheckmarkWidgetProvider.class); + updateWidgets(context, HistoryWidgetProvider.class); + updateWidgets(context, ScoreWidgetProvider.class); + updateWidgets(context, StreakWidgetProvider.class); + updateWidgets(context, FrequencyWidgetProvider.class); + } + + private static void updateWidgets(Context context, Class providerClass) + { + ComponentName provider = new ComponentName(context, providerClass); + Intent intent = new Intent(context, providerClass); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); + context.sendBroadcast(intent); + } } diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index a8dad23b8..907a28b7e 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -19,9 +19,7 @@ package org.isoron.uhabits; -import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -40,18 +38,12 @@ import android.support.v7.app.ActionBar; import android.view.Menu; import android.view.MenuItem; +import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; -import org.isoron.uhabits.widgets.FrequencyWidgetProvider; -import org.isoron.uhabits.widgets.HistoryWidgetProvider; -import org.isoron.uhabits.widgets.ScoreWidgetProvider; -import org.isoron.uhabits.widgets.StreakWidgetProvider; import java.io.File; import java.io.IOException; @@ -122,7 +114,6 @@ public class MainActivity extends BaseActivity }.execute(); } - private void showTutorial() { Boolean firstRun = prefs.getBoolean("pref_first_run", true); @@ -263,47 +254,10 @@ public class MainActivity extends BaseActivity } @Override - public void onPostExecuteCommand(Long refreshKey) + public void onPostExecuteCommand(Command command, Long refreshKey) { listHabitsFragment.onPostExecuteCommand(refreshKey); - - new BaseTask() - { - @Override - protected void doInBackground() - { - dismissNotifications(MainActivity.this); - updateWidgets(MainActivity.this); - } - }.execute(); - } - - private void dismissNotifications(Context context) - { - for(Habit h : Habit.getHabitsWithReminder()) - { - if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED) - HabitBroadcastReceiver.dismissNotification(context, h); - } - } - - public static void updateWidgets(Context context) - { - updateWidgets(context, CheckmarkWidgetProvider.class); - updateWidgets(context, HistoryWidgetProvider.class); - updateWidgets(context, ScoreWidgetProvider.class); - updateWidgets(context, StreakWidgetProvider.class); - updateWidgets(context, FrequencyWidgetProvider.class); - } - - private static void updateWidgets(Context context, Class providerClass) - { - ComponentName provider = new ComponentName(context, providerClass); - Intent intent = new Intent(context, providerClass); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - context.sendBroadcast(intent); + super.onPostExecuteCommand(command, refreshKey); } @Override @@ -331,4 +285,6 @@ public class MainActivity extends BaseActivity listHabitsFragment.showImportDialog(); } + + } diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java new file mode 100644 index 000000000..93ed0a3e0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 Á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.support.annotation.NonNull; +import android.util.Log; + +import com.github.nkzawa.emitter.Emitter; +import com.github.nkzawa.socketio.client.IO; +import com.github.nkzawa.socketio.client.Socket; + +import org.isoron.uhabits.commands.Command; +import org.isoron.uhabits.commands.ToggleRepetitionCommand; +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URISyntaxException; +import java.util.LinkedList; + +public class SyncManager +{ + public static final String EXECUTE_COMMAND = "executeCommand"; + public static final String POST_COMMAND = "postCommand"; + public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; + + private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; + private static String CLIENT_KEY; + + @NonNull + private Socket socket; + private BaseActivity activity; + private LinkedList outbox; + + public SyncManager(BaseActivity activity) + { + this.activity = activity; + outbox = new LinkedList<>(); + CLIENT_KEY = DatabaseHelper.getRandomId(); + + try + { + socket = IO.socket(SYNC_SERVER_URL); + socket.connect(); + socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); + socket.on(EXECUTE_COMMAND, new OnExecuteCommandListener()); + } + catch (URISyntaxException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public void postCommand(Command command) + { + JSONObject msg = command.toJSON(); + if(msg != null) + { + socket.emit(POST_COMMAND, msg.toString()); + outbox.add(command); + } + } + + public void close() + { + socket.close(); + } + + private class OnConnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + JSONObject authMsg = buildAuthMessage(); + socket.emit("auth", authMsg.toString()); + } + + private JSONObject buildAuthMessage() + { + try + { + JSONObject json = new JSONObject(); + json.put("group_key", GROUP_KEY); + json.put("client_key", CLIENT_KEY); + json.put("version", BuildConfig.VERSION_NAME); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private class OnExecuteCommandListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + try + { + executeCommand(args[0]); + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private void executeCommand(Object arg) throws JSONException + { + Log.d("SyncManager", String.format("Received command: %s", arg.toString())); + JSONObject root = new JSONObject(arg.toString()); + if(root.getString("command").equals("ToggleRepetition")) + { + Command received = ToggleRepetitionCommand.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + for(Command pending : outbox) + { + if(pending.getId().equals(received.getId())) + { + outbox.remove(pending); + Log.d("SyncManager", "Received command discarded"); + return; + } + } + + activity.executeCommand(received, null, false); + Log.d("SyncManager", "Received command executed"); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index b9427e38a..ec809bf03 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,8 +19,25 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONObject; + public abstract class Command { + private final String id; + + public Command() + { + id = DatabaseHelper.getRandomId(); + } + + public Command(String id) + { + this.id = id; + } + public abstract void execute(); public abstract void undo(); @@ -34,4 +51,12 @@ public abstract class Command { return null; } + + @Nullable + public JSONObject toJSON() { return null; } + + public String getId() + { + return id; + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index 451908433..df18f654b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -19,23 +19,35 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class ToggleRepetitionCommand extends Command { - private Long offset; - private Habit habit; + private final Long timestamp; + private final Habit habit; - public ToggleRepetitionCommand(Habit habit, long offset) + public ToggleRepetitionCommand(String id, Habit habit, long timestamp) { - this.offset = offset; + super(id); + this.timestamp = timestamp; + this.habit = habit; + } + + public ToggleRepetitionCommand(Habit habit, long timestamp) + { + super(); + this.timestamp = timestamp; this.habit = habit; } @Override public void execute() { - habit.repetitions.toggle(offset); + habit.repetitions.toggle(timestamp); } @Override @@ -43,4 +55,39 @@ public class ToggleRepetitionCommand extends Command { execute(); } + + @Nullable + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = new JSONObject(); + JSONObject data = new JSONObject(); + root.put("id", getId()); + root.put("command", "ToggleRepetition"); + data.put("habit", habit.getId()); + data.put("timestamp", timestamp); + root.put("data", data); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + Long habitId = data.getLong("habit"); + Long timestamp = data.getLong("timestamp"); + + Habit habit = Habit.get(habitId); + if(habit == null) return null; + + return new ToggleRepetitionCommand(id, habit, timestamp); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index d3c3d21e5..f6db065f3 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -45,7 +45,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.text.SimpleDateFormat; +import java.util.Random; public class DatabaseHelper { @@ -71,6 +73,11 @@ public class DatabaseHelper out.write(buffer, 0, numBytes); } + public static String getRandomId() + { + return new BigInteger(130, new Random()).toString(32); + } + public interface Command { void execute(); From 41d9e2f0f5ba9dba63e3d3425b27c04694e11431 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 14:10:26 -0400 Subject: [PATCH 002/187] Sync archive/unarchive commands --- .../java/org/isoron/uhabits/BaseActivity.java | 9 ++- .../java/org/isoron/uhabits/SyncManager.java | 28 ++++--- .../commands/ArchiveHabitsCommand.java | 41 ++++++++++ .../org/isoron/uhabits/commands/Command.java | 24 +++++- .../uhabits/commands/CommandParser.java | 75 +++++++++++++++++++ .../commands/ToggleRepetitionCommand.java | 7 +- .../commands/UnarchiveHabitsCommand.java | 38 ++++++++++ 7 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/commands/CommandParser.java diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index ec3fea9dc..455e51d60 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -97,12 +97,13 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U { BaseActivity.this.onPostExecuteCommand(command, refreshKey); BackupManager.dataChanged("org.isoron.uhabits"); - if(shouldBroadcast) sync.postCommand(command); + if(shouldBroadcast) + { + sync.postCommand(command); + showToast(command.getExecuteStringId()); + } } }.execute(); - - - showToast(command.getExecuteStringId()); } public void onPostExecuteCommand(Command command, Long refreshKey) diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 93ed0a3e0..1130fef5d 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -27,7 +27,7 @@ import com.github.nkzawa.socketio.client.IO; import com.github.nkzawa.socketio.client.Socket; import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.commands.ToggleRepetitionCommand; +import org.isoron.uhabits.commands.CommandParser; import org.isoron.uhabits.helpers.DatabaseHelper; import org.json.JSONException; import org.json.JSONObject; @@ -129,23 +129,21 @@ public class SyncManager { Log.d("SyncManager", String.format("Received command: %s", arg.toString())); JSONObject root = new JSONObject(arg.toString()); - if(root.getString("command").equals("ToggleRepetition")) - { - Command received = ToggleRepetitionCommand.fromJSON(root); - if(received == null) throw new RuntimeException("received is null"); - for(Command pending : outbox) + Command received = CommandParser.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + for(Command pending : outbox) + { + if(pending.getId().equals(received.getId())) { - if(pending.getId().equals(received.getId())) - { - outbox.remove(pending); - Log.d("SyncManager", "Received command discarded"); - return; - } + outbox.remove(pending); + Log.d("SyncManager", "Received command discarded"); + return; } - - activity.executeCommand(received, null, false); - Log.d("SyncManager", "Received command executed"); } + + activity.executeCommand(received, null, false); + Log.d("SyncManager", "Received command executed"); } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 25e998b7b..72086f183 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -19,9 +19,15 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class ArchiveHabitsCommand extends Command @@ -29,6 +35,12 @@ public class ArchiveHabitsCommand extends Command private List habits; + public ArchiveHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public ArchiveHabitsCommand(List habits) { this.habits = habits; @@ -55,4 +67,33 @@ public class ArchiveHabitsCommand extends Command { return R.string.toast_habit_unarchived; } + + @Nullable + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "ArchiveHabits"); + data.put("habits", CommandParser.habitsToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("habits"); + + LinkedList habits = CommandParser.habitsFromJSON(habitIds); + return new ArchiveHabitsCommand(id, habits); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index ec809bf03..375749622 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,11 +19,16 @@ package org.isoron.uhabits.commands; -import android.support.annotation.Nullable; +import android.support.annotation.NonNull; import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; +import java.util.List; + public abstract class Command { private final String id; @@ -52,8 +57,21 @@ public abstract class Command return null; } - @Nullable - public JSONObject toJSON() { return null; } + public JSONObject toJSON() + { + try + { + JSONObject root = new JSONObject(); + JSONObject data = new JSONObject(); + root.put("id", getId()); + root.put("data", data); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } public String getId() { diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java new file mode 100644 index 000000000..9267740f8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Á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.commands; + +import android.support.annotation.NonNull; + +import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.LinkedList; +import java.util.List; + +public class CommandParser +{ + public static Command fromJSON(JSONObject json) throws JSONException + { + switch(json.getString("command")) + { + case "ToggleRepetition": + return ToggleRepetitionCommand.fromJSON(json); + + case "ArchiveHabits": + return ArchiveHabitsCommand.fromJSON(json); + + case "UnarchiveHabits": + return UnarchiveHabitsCommand.fromJSON(json); + } + + return null; + } + + @NonNull + public static LinkedList habitsFromJSON(JSONArray habitIds) throws JSONException + { + LinkedList habits = new LinkedList<>(); + + for (int i = 0; i < habitIds.length(); i++) + { + Long hId = habitIds.getLong(i); + Habit h = Habit.get(hId); + if(h == null) continue; + + habits.add(h); + } + + return habits; + } + + @NonNull + protected static JSONArray habitsToJSON(List habits) + { + JSONArray habitIds = new JSONArray(); + for(Habit h : habits) habitIds.put(h.getId()); + return habitIds; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index df18f654b..e77c895db 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -56,19 +56,16 @@ public class ToggleRepetitionCommand extends Command execute(); } - @Nullable @Override public JSONObject toJSON() { try { - JSONObject root = new JSONObject(); - JSONObject data = new JSONObject(); - root.put("id", getId()); + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); root.put("command", "ToggleRepetition"); data.put("habit", habit.getId()); data.put("timestamp", timestamp); - root.put("data", data); return root; } catch (JSONException e) diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 612481fa7..3af0b7aeb 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -21,7 +21,11 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class UnarchiveHabitsCommand extends Command @@ -29,6 +33,12 @@ public class UnarchiveHabitsCommand extends Command private List habits; + public UnarchiveHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public UnarchiveHabitsCommand(List habits) { this.habits = habits; @@ -55,4 +65,32 @@ public class UnarchiveHabitsCommand extends Command { return R.string.toast_habit_archived; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "UnarchiveHabits"); + data.put("habits", CommandParser.habitsToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("habits"); + + LinkedList habits = CommandParser.habitsFromJSON(habitIds); + return new UnarchiveHabitsCommand(id, habits); + } + } \ No newline at end of file From 56e1268f8569e149d750e0ba2640387f66faf0cc Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 17:41:03 -0400 Subject: [PATCH 003/187] Sync remaining commands --- .../java/org/isoron/uhabits/BaseActivity.java | 13 ++++-- .../java/org/isoron/uhabits/SyncManager.java | 7 +-- .../commands/ArchiveHabitsCommand.java | 6 +-- .../commands/ChangeHabitColorCommand.java | 46 ++++++++++++++++++- .../uhabits/commands/CommandParser.java | 21 ++++++++- .../uhabits/commands/CreateHabitCommand.java | 39 ++++++++++++++++ .../uhabits/commands/DeleteHabitsCommand.java | 37 +++++++++++++++ .../uhabits/commands/EditHabitCommand.java | 46 +++++++++++++++++++ .../commands/UnarchiveHabitsCommand.java | 6 +-- .../java/org/isoron/uhabits/models/Habit.java | 44 ++++++++++++++++++ 10 files changed, 249 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index 455e51d60..be9ac786d 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -63,7 +63,12 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); + } + @Override + protected void onResume() + { + super.onResume(); sync = new SyncManager(this); } @@ -99,7 +104,7 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U BackupManager.dataChanged("org.isoron.uhabits"); if(shouldBroadcast) { - sync.postCommand(command); + if(sync != null) sync.postCommand(command); showToast(command.getExecuteStringId()); } } @@ -191,10 +196,12 @@ abstract public class BaseActivity extends AppCompatActivity implements Thread.U } @Override - protected void onDestroy() + protected void onPause() { sync.close(); - super.onDestroy(); + sync = null; + + super.onPause(); } private void dismissNotifications(Context context) diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 1130fef5d..055e2b4e1 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -42,7 +42,7 @@ public class SyncManager public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; - private static String CLIENT_KEY; + private static String CLIENT_ID; @NonNull private Socket socket; @@ -53,7 +53,7 @@ public class SyncManager { this.activity = activity; outbox = new LinkedList<>(); - CLIENT_KEY = DatabaseHelper.getRandomId(); + CLIENT_ID = DatabaseHelper.getRandomId(); try { @@ -80,6 +80,7 @@ public class SyncManager public void close() { + socket.off(); socket.close(); } @@ -98,7 +99,7 @@ public class SyncManager { JSONObject json = new JSONObject(); json.put("group_key", GROUP_KEY); - json.put("client_key", CLIENT_KEY); + json.put("client_id", CLIENT_ID); json.put("version", BuildConfig.VERSION_NAME); return json; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 72086f183..7f0bdea7c 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -78,7 +78,7 @@ public class ArchiveHabitsCommand extends Command JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); root.put("command", "ArchiveHabits"); - data.put("habits", CommandParser.habitsToJSON(habits)); + data.put("ids", CommandParser.habitListToJSON(habits)); return root; } catch (JSONException e) @@ -91,9 +91,9 @@ public class ArchiveHabitsCommand extends Command { String id = json.getString("id"); JSONObject data = (JSONObject) json.get("data"); - JSONArray habitIds = data.getJSONArray("habits"); + JSONArray habitIds = data.getJSONArray("ids"); - LinkedList habits = CommandParser.habitsFromJSON(habitIds); + LinkedList habits = CommandParser.habitListFromJSON(habitIds); return new ArchiveHabitsCommand(id, habits); } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java index 04ba83d7d..6d8a336ae 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -19,13 +19,15 @@ package org.isoron.uhabits.commands; -import com.activeandroid.ActiveAndroid; - import org.isoron.uhabits.R; import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; public class ChangeHabitColorCommand extends Command @@ -34,7 +36,18 @@ public class ChangeHabitColorCommand extends Command List originalColors; Integer newColor; + public ChangeHabitColorCommand(String id, List habits, Integer newColor) + { + super(id); + init(habits, newColor); + } + public ChangeHabitColorCommand(List habits, Integer newColor) + { + init(habits, newColor); + } + + private void init(List habits, Integer newColor) { this.habits = habits; this.newColor = newColor; @@ -77,4 +90,33 @@ public class ChangeHabitColorCommand extends Command { return R.string.toast_habit_changed; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "ChangeHabitColor"); + data.put("ids", CommandParser.habitListToJSON(habits)); + data.put("color", newColor); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("ids"); + int newColor = data.getInt("color"); + + LinkedList habits = CommandParser.habitListFromJSON(habitIds); + return new ChangeHabitColorCommand(id, habits, newColor); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java index 9267740f8..c39bb9e10 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -43,13 +43,30 @@ public class CommandParser case "UnarchiveHabits": return UnarchiveHabitsCommand.fromJSON(json); + + case "ChangeHabitColor": + return ChangeHabitColorCommand.fromJSON(json); + + case "CreateHabit": + return CreateHabitCommand.fromJSON(json); + + case "DeleteHabits": + return DeleteHabitsCommand.fromJSON(json); + + case "EditHabit": + return EditHabitCommand.fromJSON(json); + +// TODO: Implement this +// case "ReorderHabit": +// return ReorderHabitCommand.fromJSON(json); + } return null; } @NonNull - public static LinkedList habitsFromJSON(JSONArray habitIds) throws JSONException + public static LinkedList habitListFromJSON(JSONArray habitIds) throws JSONException { LinkedList habits = new LinkedList<>(); @@ -66,7 +83,7 @@ public class CommandParser } @NonNull - protected static JSONArray habitsToJSON(List habits) + protected static JSONArray habitListToJSON(List habits) { JSONArray habitIds = new JSONArray(); for(Habit h : habits) habitIds.put(h.getId()); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java index 7cc9ad51c..762ffbbd5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -19,14 +19,25 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class CreateHabitCommand extends Command { private Habit model; private Long savedId; + public CreateHabitCommand(String id, Habit model, Long savedId) + { + super(id); + this.model = model; + this.savedId = savedId; + } + public CreateHabitCommand(Habit model) { this.model = model; @@ -68,4 +79,32 @@ public class CreateHabitCommand extends Command return R.string.toast_habit_deleted; } + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "CreateHabit"); + data.put("habit", model.toJSON()); + data.put("id", savedId); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject root) throws JSONException + { + String commandId = root.getString("id"); + JSONObject data = (JSONObject) root.get("data"); + Habit model = Habit.fromJSON(data.getJSONObject("habit")); + Long savedId = data.getLong("id"); + + return new CreateHabitCommand(commandId, model, savedId); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java index 34e26c50c..ca8ed0a78 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -21,13 +21,23 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.LinkedList; import java.util.List; public class DeleteHabitsCommand extends Command { private List habits; + public DeleteHabitsCommand(String id, List habits) + { + super(id); + this.habits = habits; + } + public DeleteHabitsCommand(List habits) { this.habits = habits; @@ -57,4 +67,31 @@ public class DeleteHabitsCommand extends Command { return R.string.toast_habit_restored; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "DeleteHabits"); + data.put("ids", CommandParser.habitListToJSON(habits)); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Command fromJSON(JSONObject json) throws JSONException + { + String id = json.getString("id"); + JSONObject data = (JSONObject) json.get("data"); + JSONArray habitIds = data.getJSONArray("ids"); + + LinkedList habits = CommandParser.habitListFromJSON(habitIds); + return new DeleteHabitsCommand(id, habits); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 7a7787d6a..59e4a1506 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -19,8 +19,12 @@ package org.isoron.uhabits.commands; +import android.support.annotation.Nullable; + import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.json.JSONException; +import org.json.JSONObject; public class EditHabitCommand extends Command { @@ -29,7 +33,18 @@ public class EditHabitCommand extends Command private long savedId; private boolean hasIntervalChanged; + public EditHabitCommand(String id, Habit original, Habit modified) + { + super(id); + init(original, modified); + } + public EditHabitCommand(Habit original, Habit modified) + { + init(original, modified); + } + + private void init(Habit original, Habit modified) { this.savedId = original.getId(); this.modified = new Habit(modified); @@ -81,4 +96,35 @@ public class EditHabitCommand extends Command { return R.string.toast_habit_changed_back; } + + @Override + public JSONObject toJSON() + { + try + { + JSONObject root = super.toJSON(); + JSONObject data = root.getJSONObject("data"); + root.put("command", "EditHabit"); + data.put("id", savedId); + data.put("params", modified.toJSON()); + return root; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + @Nullable + public static Command fromJSON(JSONObject root) throws JSONException + { + String commandId = root.getString("id"); + JSONObject data = (JSONObject) root.get("data"); + Habit original = Habit.get(data.getLong("id")); + if(original == null) return null; + + Habit modified = Habit.fromJSON(data.getJSONObject("params")); + + return new EditHabitCommand(commandId, original, modified); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 3af0b7aeb..31fd680d5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -74,7 +74,7 @@ public class UnarchiveHabitsCommand extends Command JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); root.put("command", "UnarchiveHabits"); - data.put("habits", CommandParser.habitsToJSON(habits)); + data.put("ids", CommandParser.habitListToJSON(habits)); return root; } catch (JSONException e) @@ -87,9 +87,9 @@ public class UnarchiveHabitsCommand extends Command { String id = json.getString("id"); JSONObject data = (JSONObject) json.get("data"); - JSONArray habitIds = data.getJSONArray("habits"); + JSONArray habitIds = data.getJSONArray("ids"); - LinkedList habits = CommandParser.habitsFromJSON(habitIds); + LinkedList habits = CommandParser.habitListFromJSON(habitIds); return new UnarchiveHabitsCommand(id, habits); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 1fdb982ad..e97540331 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -37,6 +37,8 @@ import com.opencsv.CSVWriter; import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.DateHelper; +import org.json.JSONException; +import org.json.JSONObject; import java.io.IOException; import java.io.Writer; @@ -510,4 +512,46 @@ public class Habit extends Model csv.close(); } + + public JSONObject toJSON() + { + try + { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("description", description); + json.put("freqNum", freqNum); + json.put("freqDen", freqDen); + json.put("color", color); + json.put("position", position); + json.put("reminderHour", reminderHour); + json.put("reminderMin", reminderMin); + json.put("reminderDays", reminderDays); + json.put("archived", archived); + return json; + } + catch(JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + public static Habit fromJSON(JSONObject json) throws JSONException + { + Habit habit = new Habit(); + habit.name = json.getString("name"); + habit.description = json.getString("description"); + habit.freqNum = json.getInt("freqNum"); + habit.freqDen = json.getInt("freqDen"); + habit.color = json.getInt("color"); + habit.position = json.getInt("position"); + habit.archived = json.getInt("archived"); + if(json.has("reminderHour")) + { + habit.reminderHour = json.getInt("reminderHour"); + habit.reminderMin = json.getInt("reminderMin"); + habit.reminderDays = json.getInt("reminderDays"); + } + return habit; + } } From b0040bd83c72ace15f4371941458ae4fea082936 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 14 May 2016 23:19:14 -0400 Subject: [PATCH 004/187] Fetch commands since last sync --- app/build.gradle | 6 ++- .../java/org/isoron/uhabits/SyncManager.java | 47 +++++++++++++++---- .../uhabits/fragments/SettingsFragment.java | 10 ++++ app/src/main/res/xml/preferences.xml | 11 +++++ 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c7dc17682..47131224d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,11 @@ dependencies { compile 'com.github.paolorotolo:appintro:3.4.0' compile 'org.apmem.tools:layouts:1.10@aar' compile 'com.opencsv:opencsv:3.7' - compile 'com.github.nkzawa:socket.io-client:0.3.0' + + compile ('io.socket:socket.io-client:0.7.0') { + exclude group: 'org.json', module: 'json' + } + compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java index 055e2b4e1..bbfbf73b6 100644 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/SyncManager.java @@ -19,13 +19,11 @@ package org.isoron.uhabits; +import android.content.SharedPreferences; import android.support.annotation.NonNull; +import android.support.v7.preference.PreferenceManager; import android.util.Log; -import com.github.nkzawa.emitter.Emitter; -import com.github.nkzawa.socketio.client.IO; -import com.github.nkzawa.socketio.client.Socket; - import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.CommandParser; import org.isoron.uhabits.helpers.DatabaseHelper; @@ -35,14 +33,19 @@ import org.json.JSONObject; import java.net.URISyntaxException; import java.util.LinkedList; +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + public class SyncManager { public static final String EXECUTE_COMMAND = "executeCommand"; public static final String POST_COMMAND = "postCommand"; - public static final String SYNC_SERVER_URL = "http://10.0.2.2:4000"; + public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; - private static String GROUP_KEY = "sEBY3poXHFH7EyB43V2JoQUNEtBjMgdD"; + private static String GROUP_KEY; private static String CLIENT_ID; + private final SharedPreferences prefs; @NonNull private Socket socket; @@ -53,6 +56,9 @@ public class SyncManager { this.activity = activity; outbox = new LinkedList<>(); + + prefs = PreferenceManager.getDefaultSharedPreferences(activity); + GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); CLIENT_ID = DatabaseHelper.getRandomId(); try @@ -91,6 +97,24 @@ public class SyncManager { JSONObject authMsg = buildAuthMessage(); socket.emit("auth", authMsg.toString()); + + Long lastSync = prefs.getLong("lastSync", 0); + JSONObject fetchMsg = buildFetchMessage(lastSync); + socket.emit("fetchCommands", fetchMsg.toString()); + } + + private JSONObject buildFetchMessage(Long lastSync) + { + try + { + JSONObject json = new JSONObject(); + json.put("since", lastSync); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } } private JSONObject buildAuthMessage() @@ -98,8 +122,8 @@ public class SyncManager try { JSONObject json = new JSONObject(); - json.put("group_key", GROUP_KEY); - json.put("client_id", CLIENT_ID); + json.put("groupKey", GROUP_KEY); + json.put("clientId", CLIENT_ID); json.put("version", BuildConfig.VERSION_NAME); return json; } @@ -117,6 +141,8 @@ public class SyncManager { try { + JSONObject root = new JSONObject(args[0].toString()); + updateLastSync(root.getLong("timestamp")); executeCommand(args[0]); } catch (JSONException e) @@ -147,4 +173,9 @@ public class SyncManager activity.executeCommand(received, null, false); Log.d("SyncManager", "Received command executed"); } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java index 838527589..cbc56333b 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java @@ -49,6 +49,7 @@ public class SettingsFragment extends PreferenceFragmentCompat setResultOnPreferenceClick("bugReport", MainActivity.RESULT_BUG_REPORT); updateRingtoneDescription(); + updateSyncDescription(); if(UIHelper.isLocaleFullyTranslated()) removePreference("translate", "linksCategory"); @@ -101,9 +102,18 @@ public class SettingsFragment extends PreferenceFragmentCompat @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals("syncKey")) + updateSyncDescription(); + BackupManager.dataChanged("org.isoron.uhabits"); } + private void updateSyncDescription() + { + SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); + findPreference("syncKey").setSummary(preferences.getString("syncKey", "")); + } + @Override public boolean onPreferenceTreeClick(Preference preference) { diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 1339b2d05..5a474d829 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -88,6 +88,17 @@ + + + + + + From e3b7e9f60f37f24362b84f9cba51f0cdad516472 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 16 May 2016 08:20:08 -0400 Subject: [PATCH 005/187] Persist pending events to database --- app/build.gradle | 2 +- .../java/org/isoron/uhabits/BaseActivity.java | 1 + .../java/org/isoron/uhabits/SyncManager.java | 181 ------------ .../uhabits/helpers/DatabaseHelper.java | 3 +- .../java/org/isoron/uhabits/sync/Event.java | 65 +++++ .../org/isoron/uhabits/sync/SyncManager.java | 270 ++++++++++++++++++ 6 files changed, 339 insertions(+), 183 deletions(-) delete mode 100644 app/src/main/java/org/isoron/uhabits/SyncManager.java create mode 100644 app/src/main/java/org/isoron/uhabits/sync/Event.java create mode 100644 app/src/main/java/org/isoron/uhabits/sync/SyncManager.java diff --git a/app/build.gradle b/app/build.gradle index 47131224d..848e68e00 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 15 targetSdkVersion 23 - buildConfigField "Integer", "databaseVersion", "14" + buildConfigField "Integer", "databaseVersion", "16" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java index be9ac786d..4b0398495 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ b/app/src/main/java/org/isoron/uhabits/BaseActivity.java @@ -40,6 +40,7 @@ import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.UIHelper; import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.sync.SyncManager; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; import org.isoron.uhabits.widgets.FrequencyWidgetProvider; diff --git a/app/src/main/java/org/isoron/uhabits/SyncManager.java b/app/src/main/java/org/isoron/uhabits/SyncManager.java deleted file mode 100644 index bbfbf73b6..000000000 --- a/app/src/main/java/org/isoron/uhabits/SyncManager.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2016 Á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.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.v7.preference.PreferenceManager; -import android.util.Log; - -import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.commands.CommandParser; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.json.JSONException; -import org.json.JSONObject; - -import java.net.URISyntaxException; -import java.util.LinkedList; - -import io.socket.client.IO; -import io.socket.client.Socket; -import io.socket.emitter.Emitter; - -public class SyncManager -{ - public static final String EXECUTE_COMMAND = "executeCommand"; - public static final String POST_COMMAND = "postCommand"; - public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; - - private static String GROUP_KEY; - private static String CLIENT_ID; - private final SharedPreferences prefs; - - @NonNull - private Socket socket; - private BaseActivity activity; - private LinkedList outbox; - - public SyncManager(BaseActivity activity) - { - this.activity = activity; - outbox = new LinkedList<>(); - - prefs = PreferenceManager.getDefaultSharedPreferences(activity); - GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); - CLIENT_ID = DatabaseHelper.getRandomId(); - - try - { - socket = IO.socket(SYNC_SERVER_URL); - socket.connect(); - socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); - socket.on(EXECUTE_COMMAND, new OnExecuteCommandListener()); - } - catch (URISyntaxException e) - { - throw new RuntimeException(e.getMessage()); - } - } - - public void postCommand(Command command) - { - JSONObject msg = command.toJSON(); - if(msg != null) - { - socket.emit(POST_COMMAND, msg.toString()); - outbox.add(command); - } - } - - public void close() - { - socket.off(); - socket.close(); - } - - private class OnConnectListener implements Emitter.Listener - { - @Override - public void call(Object... args) - { - JSONObject authMsg = buildAuthMessage(); - socket.emit("auth", authMsg.toString()); - - Long lastSync = prefs.getLong("lastSync", 0); - JSONObject fetchMsg = buildFetchMessage(lastSync); - socket.emit("fetchCommands", fetchMsg.toString()); - } - - private JSONObject buildFetchMessage(Long lastSync) - { - try - { - JSONObject json = new JSONObject(); - json.put("since", lastSync); - return json; - } - catch (JSONException e) - { - throw new RuntimeException(e.getMessage()); - } - } - - private JSONObject buildAuthMessage() - { - try - { - JSONObject json = new JSONObject(); - json.put("groupKey", GROUP_KEY); - json.put("clientId", CLIENT_ID); - json.put("version", BuildConfig.VERSION_NAME); - return json; - } - catch (JSONException e) - { - throw new RuntimeException(e.getMessage()); - } - } - } - - private class OnExecuteCommandListener implements Emitter.Listener - { - @Override - public void call(Object... args) - { - try - { - JSONObject root = new JSONObject(args[0].toString()); - updateLastSync(root.getLong("timestamp")); - executeCommand(args[0]); - } - catch (JSONException e) - { - throw new RuntimeException(e.getMessage()); - } - } - } - - private void executeCommand(Object arg) throws JSONException - { - Log.d("SyncManager", String.format("Received command: %s", arg.toString())); - JSONObject root = new JSONObject(arg.toString()); - - Command received = CommandParser.fromJSON(root); - if(received == null) throw new RuntimeException("received is null"); - - for(Command pending : outbox) - { - if(pending.getId().equals(received.getId())) - { - outbox.remove(pending); - Log.d("SyncManager", "Received command discarded"); - return; - } - } - - activity.executeCommand(received, null, false); - Log.d("SyncManager", "Received command executed"); - } - - private void updateLastSync(Long timestamp) - { - prefs.edit().putLong("lastSync", timestamp).apply(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index f6db065f3..a453a5543 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -38,6 +38,7 @@ import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Streak; +import org.isoron.uhabits.sync.Event; import java.io.File; import java.io.FileInputStream; @@ -201,7 +202,7 @@ public class DatabaseHelper .setDatabaseName(getDatabaseFilename()) .setDatabaseVersion(BuildConfig.databaseVersion) .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, - Streak.class) + Streak.class, Event.class) .create(); ActiveAndroid.initialize(dbConfig); diff --git a/app/src/main/java/org/isoron/uhabits/sync/Event.java b/app/src/main/java/org/isoron/uhabits/sync/Event.java new file mode 100644 index 000000000..67308dc72 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/sync/Event.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Á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.sync; + +import android.support.annotation.NonNull; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Select; + +import java.util.List; + +@Table(name = "Events") +public class Event extends Model +{ + @NonNull + @Column(name = "timestamp") + public Long timestamp; + + @NonNull + @Column(name = "message") + public String message; + + @NonNull + @Column(name = "serverId") + public String serverId; + + public Event() + { + timestamp = 0L; + message = ""; + serverId = ""; + } + + public Event(@NonNull String serverId, long timestamp, @NonNull String message) + { + this.serverId = serverId; + this.timestamp = timestamp; + this.message = message; + } + + @NonNull + public static List getAll() + { + return new Select().from(Event.class).orderBy("timestamp").execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java new file mode 100644 index 000000000..dfddc89c2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2016 Á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.sync; + +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import android.support.v7.preference.PreferenceManager; +import android.util.Log; + +import org.isoron.uhabits.BaseActivity; +import org.isoron.uhabits.BuildConfig; +import org.isoron.uhabits.commands.Command; +import org.isoron.uhabits.commands.CommandParser; +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URISyntaxException; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + +public class SyncManager +{ + public static final String EVENT_AUTH = "auth"; + public static final String EVENT_AUTH_OK = "authOK"; + public static final String EVENT_EXECUTE_COMMAND = "execute"; + public static final String EVENT_POST_COMMAND = "post"; + public static final String EVENT_FETCH = "fetch"; + public static final String EVENT_FETCH_OK = "fetchOK"; + + public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; + + private static String GROUP_KEY; + private static String CLIENT_ID; + private final SharedPreferences prefs; + + @NonNull + private Socket socket; + private BaseActivity activity; + private LinkedList pendingConfirmation; + private List pendingEmit; + private boolean readyToEmit = false; + + public SyncManager(final BaseActivity activity) + { + this.activity = activity; + pendingConfirmation = new LinkedList<>(); + pendingEmit = Event.getAll(); + + prefs = PreferenceManager.getDefaultSharedPreferences(activity); + GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); + CLIENT_ID = DatabaseHelper.getRandomId(); + + try + { + socket = IO.socket(SYNC_SERVER_URL); + + logSocketEvent(socket, Socket.EVENT_CONNECT, "Connected"); + logSocketEvent(socket, Socket.EVENT_CONNECT_TIMEOUT, "Connect timeout"); + logSocketEvent(socket, Socket.EVENT_CONNECTING, "Connecting..."); + logSocketEvent(socket, Socket.EVENT_CONNECT_ERROR, "Connect error"); + logSocketEvent(socket, Socket.EVENT_DISCONNECT, "Disconnected"); + logSocketEvent(socket, Socket.EVENT_RECONNECT, "Reconnected"); + logSocketEvent(socket, Socket.EVENT_RECONNECT_ATTEMPT, "Reconnecting..."); + logSocketEvent(socket, Socket.EVENT_RECONNECT_ERROR, "Reconnect error"); + logSocketEvent(socket, Socket.EVENT_RECONNECT_FAILED, "Reconnect failed"); + logSocketEvent(socket, Socket.EVENT_DISCONNECT, "Disconnected"); + logSocketEvent(socket, Socket.EVENT_PING, "Ping"); + logSocketEvent(socket, Socket.EVENT_PONG, "Pong"); + + socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); + socket.on(Socket.EVENT_DISCONNECT, new OnDisconnectListener()); + socket.on(EVENT_EXECUTE_COMMAND, new OnExecuteCommandListener()); + socket.on(EVENT_AUTH_OK, new OnAuthOKListener()); + socket.on(EVENT_FETCH_OK, new OnFetchOKListener()); + + socket.connect(); + } + catch (URISyntaxException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + private void logSocketEvent(Socket socket, String event, final String msg) + { + socket.on(event, new Emitter.Listener() + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", msg); + } + }); + } + + public void postCommand(Command command) + { + JSONObject msg = command.toJSON(); + if(msg == null) return; + + Long now = new Date().getTime(); + Event e = new Event(command.getId(), now, msg.toString()); + e.save(); + + Log.i("SyncManager", "Adding to outbox: " + msg.toString()); + pendingEmit.add(e); + if(readyToEmit) emitPending(); + } + + private void emitPending() + { + for (Event e : pendingEmit) + { + Log.i("SyncManager", "Emitting: " + e.message); + socket.emit(EVENT_POST_COMMAND, e.message); + pendingConfirmation.add(e); + } + + pendingEmit.clear(); + } + + public void close() + { + socket.off(); + socket.close(); + } + + private class OnConnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", "Sending auth message"); + JSONObject authMsg = buildAuthMessage(); + socket.emit(EVENT_AUTH, authMsg.toString()); + } + + private JSONObject buildAuthMessage() + { + try + { + JSONObject json = new JSONObject(); + json.put("groupKey", GROUP_KEY); + json.put("clientId", CLIENT_ID); + json.put("version", BuildConfig.VERSION_NAME); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private class OnExecuteCommandListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + try + { + Log.d("SyncManager", String.format("Received command: %s", args[0].toString())); + + JSONObject root = new JSONObject(args[0].toString()); + updateLastSync(root.getLong("timestamp")); + executeCommand(root); + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } + + private void executeCommand(JSONObject root) throws JSONException + { + Command received = CommandParser.fromJSON(root); + if(received == null) throw new RuntimeException("received is null"); + + for(Event e : pendingConfirmation) + { + if(e.serverId.equals(received.getId())) + { + Log.i("SyncManager", "Pending command confirmed"); + pendingConfirmation.remove(e); + e.delete(); + return; + } + } + + Log.d("SyncManager", "Executing received command"); + activity.executeCommand(received, null, false); + } + } + + private class OnAuthOKListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", "Auth OK"); + Log.i("SyncManager", "Requesting commands since last sync"); + + Long lastSync = prefs.getLong("lastSync", 0); + JSONObject fetchMsg = buildFetchMessage(lastSync); + socket.emit(EVENT_FETCH, fetchMsg.toString()); + } + + private JSONObject buildFetchMessage(Long lastSync) + { + try + { + JSONObject json = new JSONObject(); + json.put("since", lastSync); + return json; + } + catch (JSONException e) + { + throw new RuntimeException(e.getMessage()); + } + } + } + + private class OnFetchOKListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + Log.i("SyncManager", "Fetch OK"); + emitPending(); + readyToEmit = true; + } + } + + private class OnDisconnectListener implements Emitter.Listener + { + @Override + public void call(Object... args) + { + readyToEmit = false; + } + } +} From 5b402478e986a0883dee433ca2fe5f23e6365193 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 16 May 2016 22:29:10 -0400 Subject: [PATCH 006/187] Use encrypted connection to server (TLS) --- app/src/main/assets/cacert.pem | 41 +++++++++++ .../org/isoron/uhabits/sync/SyncManager.java | 73 +++++++++++++++---- 2 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 app/src/main/assets/cacert.pem diff --git a/app/src/main/assets/cacert.pem b/app/src/main/assets/cacert.pem new file mode 100644 index 000000000..e7dfc8294 --- /dev/null +++ b/app/src/main/assets/cacert.pem @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index dfddc89c2..c6c42ce40 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -32,11 +32,18 @@ import org.isoron.uhabits.helpers.DatabaseHelper; import org.json.JSONException; import org.json.JSONObject; +import java.io.InputStream; import java.net.URISyntaxException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; import java.util.Date; import java.util.LinkedList; import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + import io.socket.client.IO; import io.socket.client.Socket; import io.socket.emitter.Emitter; @@ -50,7 +57,7 @@ public class SyncManager public static final String EVENT_FETCH = "fetch"; public static final String EVENT_FETCH_OK = "fetchOK"; - public static final String SYNC_SERVER_URL = "http://sync.loophabits.org:4000"; + public static final String SYNC_SERVER_URL = "https://sync.loophabits.org:4000"; private static String GROUP_KEY; private static String CLIENT_ID; @@ -75,6 +82,8 @@ public class SyncManager try { + IO.setDefaultSSLContext(getCACertSSLContext()); + socket = IO.socket(SYNC_SERVER_URL); logSocketEvent(socket, Socket.EVENT_CONNECT, "Connected"); @@ -100,7 +109,34 @@ public class SyncManager } catch (URISyntaxException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); + } + } + + private SSLContext getCACertSSLContext() + { + try + { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream caInput = activity.getAssets().open("cacert.pem"); + Certificate ca = cf.generateCertificate(caInput); + + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, null); + ks.setCertificateEntry("ca", ca); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + + return ctx; + } + catch (Exception e) + { + throw new RuntimeException(e); } } @@ -170,7 +206,7 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } } @@ -190,15 +226,10 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } - private void updateLastSync(Long timestamp) - { - prefs.edit().putLong("lastSync", timestamp).apply(); - } - private void executeCommand(JSONObject root) throws JSONException { Command received = CommandParser.fromJSON(root); @@ -243,7 +274,7 @@ public class SyncManager } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e); } } } @@ -253,9 +284,20 @@ public class SyncManager @Override public void call(Object... args) { - Log.i("SyncManager", "Fetch OK"); - emitPending(); - readyToEmit = true; + try + { + Log.i("SyncManager", "Fetch OK"); + + JSONObject json = new JSONObject((String) args[0]); + updateLastSync(json.getLong("timestamp")); + + emitPending(); + readyToEmit = true; + } + catch (JSONException e) + { + throw new RuntimeException(e); + } } } @@ -267,4 +309,9 @@ public class SyncManager readyToEmit = false; } } + + private void updateLastSync(Long timestamp) + { + prefs.edit().putLong("lastSync", timestamp).apply(); + } } From 2399dccddc912186b20c1a459285e0b5ba830710 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 19 May 2016 10:35:44 -0400 Subject: [PATCH 007/187] Emit JSON object instead of string --- .../org/isoron/uhabits/sync/SyncManager.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index c6c42ce40..92f8f98c7 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -162,20 +162,28 @@ public class SyncManager e.save(); Log.i("SyncManager", "Adding to outbox: " + msg.toString()); + pendingEmit.add(e); if(readyToEmit) emitPending(); } private void emitPending() { - for (Event e : pendingEmit) + try { - Log.i("SyncManager", "Emitting: " + e.message); - socket.emit(EVENT_POST_COMMAND, e.message); - pendingConfirmation.add(e); - } + for (Event e : pendingEmit) + { + Log.i("SyncManager", "Emitting: " + e.message); + socket.emit(EVENT_POST_COMMAND, new JSONObject(e.message)); + pendingConfirmation.add(e); + } - pendingEmit.clear(); + pendingEmit.clear(); + } + catch (JSONException e) + { + throw new RuntimeException(e); + } } public void close() @@ -190,8 +198,7 @@ public class SyncManager public void call(Object... args) { Log.i("SyncManager", "Sending auth message"); - JSONObject authMsg = buildAuthMessage(); - socket.emit(EVENT_AUTH, authMsg.toString()); + socket.emit(EVENT_AUTH, buildAuthMessage()); } private JSONObject buildAuthMessage() @@ -219,7 +226,6 @@ public class SyncManager try { Log.d("SyncManager", String.format("Received command: %s", args[0].toString())); - JSONObject root = new JSONObject(args[0].toString()); updateLastSync(root.getLong("timestamp")); executeCommand(root); @@ -260,8 +266,7 @@ public class SyncManager Log.i("SyncManager", "Requesting commands since last sync"); Long lastSync = prefs.getLong("lastSync", 0); - JSONObject fetchMsg = buildFetchMessage(lastSync); - socket.emit(EVENT_FETCH, fetchMsg.toString()); + socket.emit(EVENT_FETCH, buildFetchMessage(lastSync)); } private JSONObject buildFetchMessage(Long lastSync) From e6e80b98419d4ae67865697eec69da97f6c7fa2e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 22 May 2016 18:28:06 -0400 Subject: [PATCH 008/187] Update sync protocol --- .../uhabits/commands/ArchiveHabitsCommand.java | 2 +- .../uhabits/commands/ChangeHabitColorCommand.java | 2 +- .../org/isoron/uhabits/commands/CommandParser.java | 2 +- .../uhabits/commands/CreateHabitCommand.java | 2 +- .../uhabits/commands/DeleteHabitsCommand.java | 2 +- .../isoron/uhabits/commands/EditHabitCommand.java | 2 +- .../uhabits/commands/ToggleRepetitionCommand.java | 2 +- .../uhabits/commands/UnarchiveHabitsCommand.java | 2 +- .../org/isoron/uhabits/helpers/DatabaseHelper.java | 2 +- .../java/org/isoron/uhabits/sync/SyncManager.java | 14 ++++++++------ 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 7f0bdea7c..697b3cc10 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -77,7 +77,7 @@ public class ArchiveHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ArchiveHabits"); + root.put("event", "ArchiveHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java index 6d8a336ae..437594ea5 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -98,7 +98,7 @@ public class ChangeHabitColorCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ChangeHabitColor"); + root.put("event", "ChangeHabitColor"); data.put("ids", CommandParser.habitListToJSON(habits)); data.put("color", newColor); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java index c39bb9e10..34a02f981 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandParser.java @@ -33,7 +33,7 @@ public class CommandParser { public static Command fromJSON(JSONObject json) throws JSONException { - switch(json.getString("command")) + switch(json.getString("event")) { case "ToggleRepetition": return ToggleRepetitionCommand.fromJSON(json); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java index 762ffbbd5..c2d23cf66 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -86,7 +86,7 @@ public class CreateHabitCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "CreateHabit"); + root.put("event", "CreateHabit"); data.put("habit", model.toJSON()); data.put("id", savedId); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java index ca8ed0a78..1a851e94b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -75,7 +75,7 @@ public class DeleteHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "DeleteHabits"); + root.put("event", "DeleteHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 59e4a1506..5c8dcc4d4 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -104,7 +104,7 @@ public class EditHabitCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "EditHabit"); + root.put("event", "EditHabit"); data.put("id", savedId); data.put("params", modified.toJSON()); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index e77c895db..6dffa4781 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -63,7 +63,7 @@ public class ToggleRepetitionCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "ToggleRepetition"); + root.put("event", "ToggleRepetition"); data.put("habit", habit.getId()); data.put("timestamp", timestamp); return root; diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 31fd680d5..d15a32810 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -73,7 +73,7 @@ public class UnarchiveHabitsCommand extends Command { JSONObject root = super.toJSON(); JSONObject data = root.getJSONObject("data"); - root.put("command", "UnarchiveHabits"); + root.put("event", "UnarchiveHabits"); data.put("ids", CommandParser.habitListToJSON(habits)); return root; } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index a453a5543..b94a6902c 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -76,7 +76,7 @@ public class DatabaseHelper public static String getRandomId() { - return new BigInteger(130, new Random()).toString(32); + return new BigInteger(260, new Random()).toString(32).substring(0, 32); } public interface Command diff --git a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java index 92f8f98c7..e8c05e8b6 100644 --- a/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java +++ b/app/src/main/java/org/isoron/uhabits/sync/SyncManager.java @@ -52,8 +52,8 @@ public class SyncManager { public static final String EVENT_AUTH = "auth"; public static final String EVENT_AUTH_OK = "authOK"; - public static final String EVENT_EXECUTE_COMMAND = "execute"; - public static final String EVENT_POST_COMMAND = "post"; + public static final String EVENT_EXECUTE_EVENT = "execute"; + public static final String EVENT_POST_EVENT = "postEvent"; public static final String EVENT_FETCH = "fetch"; public static final String EVENT_FETCH_OK = "fetchOK"; @@ -80,6 +80,8 @@ public class SyncManager GROUP_KEY = prefs.getString("syncKey", DatabaseHelper.getRandomId()); CLIENT_ID = DatabaseHelper.getRandomId(); + Log.d("SyncManager", DatabaseHelper.getRandomId()); + try { IO.setDefaultSSLContext(getCACertSSLContext()); @@ -101,7 +103,7 @@ public class SyncManager socket.on(Socket.EVENT_CONNECT, new OnConnectListener()); socket.on(Socket.EVENT_DISCONNECT, new OnDisconnectListener()); - socket.on(EVENT_EXECUTE_COMMAND, new OnExecuteCommandListener()); + socket.on(EVENT_EXECUTE_EVENT, new OnExecuteCommandListener()); socket.on(EVENT_AUTH_OK, new OnAuthOKListener()); socket.on(EVENT_FETCH_OK, new OnFetchOKListener()); @@ -174,7 +176,7 @@ public class SyncManager for (Event e : pendingEmit) { Log.i("SyncManager", "Emitting: " + e.message); - socket.emit(EVENT_POST_COMMAND, new JSONObject(e.message)); + socket.emit(EVENT_POST_EVENT, new JSONObject(e.message)); pendingConfirmation.add(e); } @@ -293,7 +295,7 @@ public class SyncManager { Log.i("SyncManager", "Fetch OK"); - JSONObject json = new JSONObject((String) args[0]); + JSONObject json = (JSONObject) args[0]; updateLastSync(json.getLong("timestamp")); emitPending(); @@ -317,6 +319,6 @@ public class SyncManager private void updateLastSync(Long timestamp) { - prefs.edit().putLong("lastSync", timestamp).apply(); + prefs.edit().putLong("lastSync", timestamp + 1).apply(); } } From c20d5c87297d8597d3060a87822e6b8cb93a6509 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Nov 2016 20:06:28 -0500 Subject: [PATCH 009/187] Make model classes thread-safe --- app/build.gradle | 1 + .../org/isoron/uhabits/models/Checkmark.java | 3 + .../isoron/uhabits/models/CheckmarkList.java | 25 ++++++--- .../org/isoron/uhabits/models/Frequency.java | 3 + .../java/org/isoron/uhabits/models/Habit.java | 56 ++++++++++--------- .../org/isoron/uhabits/models/HabitList.java | 5 +- .../uhabits/models/ModelObservable.java | 9 ++- .../uhabits/tasks/AndroidTaskRunner.java | 2 +- 8 files changed, 65 insertions(+), 39 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6bdb20d23..f15b72924 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,7 @@ dependencies { compile 'com.opencsv:opencsv:3.7' compile 'org.apmem.tools:layouts:1.10@aar' compile 'org.jetbrains:annotations-java5:15.0' + compile 'com.google.code.findbugs:jsr305:2.0.1' provided 'javax.annotation:jsr250-api:1.0' diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index 3ae1c4b1e..617a09db4 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -21,6 +21,8 @@ package org.isoron.uhabits.models; import org.apache.commons.lang3.builder.*; +import javax.annotation.concurrent.*; + /** * A Checkmark represents the completion status of the habit for a given day. *

@@ -30,6 +32,7 @@ import org.apache.commons.lang3.builder.*; *

* Checkmarks are computed automatically from the list of repetitions. */ +@ThreadSafe public final class Checkmark { /** diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 86488a8d1..2f97d7fd3 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -27,18 +27,22 @@ import java.io.*; import java.text.*; import java.util.*; +import javax.annotation.concurrent.*; + /** * The collection of {@link Checkmark}s belonging to a habit. */ +@ThreadSafe public abstract class CheckmarkList { - protected Habit habit; + protected final Habit habit; - public ModelObservable observable = new ModelObservable(); + public final ModelObservable observable; public CheckmarkList(Habit habit) { this.habit = habit; + this.observable = new ModelObservable(); } /** @@ -64,7 +68,7 @@ public abstract class CheckmarkList * @return values for the checkmarks in the interval */ @NonNull - public final int[] getAllValues() + public synchronized final int[] getAllValues() { Repetition oldestRep = habit.getRepetitions().getOldest(); if (oldestRep == null) return new int[0]; @@ -97,7 +101,7 @@ public abstract class CheckmarkList * @return checkmark for today */ @Nullable - public final Checkmark getToday() + public synchronized final Checkmark getToday() { computeAll(); return getNewestComputed(); @@ -108,7 +112,7 @@ public abstract class CheckmarkList * * @return value of today's checkmark */ - public final int getTodayValue() + public synchronized final int getTodayValue() { Checkmark today = getToday(); if (today != null) return today.getValue(); @@ -159,9 +163,14 @@ public abstract class CheckmarkList */ public final void writeCSV(Writer out) throws IOException { - computeAll(); + int values[]; + + synchronized (this) + { + computeAll(); + values = getAllValues(); + } - int values[] = getAllValues(); long timestamp = DateUtils.getStartOfToday(); SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); @@ -271,7 +280,7 @@ public abstract class CheckmarkList * repetition of the habit until today. Days that already have a * corresponding checkmark are skipped. */ - protected final void computeAll() + private synchronized void computeAll() { Repetition oldest = habit.getRepetitions().getOldest(); if (oldest == null) return; diff --git a/app/src/main/java/org/isoron/uhabits/models/Frequency.java b/app/src/main/java/org/isoron/uhabits/models/Frequency.java index 5b893b5a1..b21349b18 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Frequency.java +++ b/app/src/main/java/org/isoron/uhabits/models/Frequency.java @@ -21,9 +21,12 @@ package org.isoron.uhabits.models; import org.apache.commons.lang3.builder.*; +import javax.annotation.concurrent.*; + /** * Represents how often is the habit repeated. */ +@ThreadSafe public class Frequency { public static final Frequency DAILY = new Frequency(1, 1); diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 5ba711407..ea0df6f7b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -26,11 +26,13 @@ import org.apache.commons.lang3.builder.*; import java.util.*; +import javax.annotation.concurrent.*; import javax.inject.*; /** * The thing that the user wants to track. */ +@ThreadSafe public class Habit { public static final String HABIT_URI_FORMAT = @@ -54,22 +56,23 @@ public class Habit @NonNull private boolean archived; - @NonNull - private StreakList streaks; + @Nullable + private Reminder reminder; @NonNull - private ScoreList scores; + private final StreakList streaks; @NonNull - private RepetitionList repetitions; + private final ScoreList scores; @NonNull - private CheckmarkList checkmarks; + private final RepetitionList repetitions; - @Nullable - private Reminder reminder; + @NonNull + private final CheckmarkList checkmarks; - private ModelObservable observable = new ModelObservable(); + @NonNull + private final ModelObservable observable; /** * Constructs a habit with default attributes. @@ -88,12 +91,13 @@ public class Habit streaks = factory.buildStreakList(this); scores = factory.buildScoreList(this); repetitions = factory.buildRepetitionList(this); + observable = new ModelObservable(); } /** * Clears the reminder for a habit. */ - public void clearReminder() + public synchronized void clearReminder() { reminder = null; observable.notifyListeners(); @@ -104,7 +108,7 @@ public class Habit * * @param model the model whose attributes should be copied from */ - public void copyFrom(@NonNull Habit model) + public synchronized void copyFrom(@NonNull Habit model) { this.name = model.getName(); this.description = model.getDescription(); @@ -119,7 +123,7 @@ public class Habit * List of checkmarks belonging to this habit. */ @NonNull - public CheckmarkList getCheckmarks() + public synchronized CheckmarkList getCheckmarks() { return checkmarks; } @@ -133,56 +137,56 @@ public class Habit * habit.color). */ @NonNull - public Integer getColor() + public synchronized Integer getColor() { return color; } - public void setColor(@NonNull Integer color) + public synchronized void setColor(@NonNull Integer color) { this.color = color; } @NonNull - public String getDescription() + public synchronized String getDescription() { return description; } - public void setDescription(@NonNull String description) + public synchronized void setDescription(@NonNull String description) { this.description = description; } @NonNull - public Frequency getFrequency() + public synchronized Frequency getFrequency() { return frequency; } - public void setFrequency(@NonNull Frequency frequency) + public synchronized void setFrequency(@NonNull Frequency frequency) { this.frequency = frequency; } @Nullable - public Long getId() + public synchronized Long getId() { return id; } - public void setId(@Nullable Long id) + public synchronized void setId(@Nullable Long id) { this.id = id; } @NonNull - public String getName() + public synchronized String getName() { return name; } - public void setName(@NonNull String name) + public synchronized void setName(@NonNull String name) { this.name = name; } @@ -203,13 +207,13 @@ public class Habit * @throws IllegalStateException if habit has no reminder */ @NonNull - public Reminder getReminder() + public synchronized Reminder getReminder() { if (reminder == null) throw new IllegalStateException(); return reminder; } - public void setReminder(@Nullable Reminder reminder) + public synchronized void setReminder(@Nullable Reminder reminder) { this.reminder = reminder; } @@ -248,17 +252,17 @@ public class Habit * * @return true if habit has reminder, false otherwise */ - public boolean hasReminder() + public synchronized boolean hasReminder() { return reminder != null; } - public boolean isArchived() + public synchronized boolean isArchived() { return archived; } - public void setArchived(boolean archived) + public synchronized void setArchived(boolean archived) { this.archived = archived; } diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitList.java b/app/src/main/java/org/isoron/uhabits/models/HabitList.java index 444a5824c..2892a2c30 100644 --- a/app/src/main/java/org/isoron/uhabits/models/HabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -28,12 +28,15 @@ import org.isoron.uhabits.utils.*; import java.io.*; import java.util.*; +import javax.annotation.concurrent.*; + /** * An ordered collection of {@link Habit}s. */ +@ThreadSafe public abstract class HabitList implements Iterable { - private ModelObservable observable; + private final ModelObservable observable; @NonNull protected final HabitMatcher filter; diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java index a762b5f8a..6ab80813b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java +++ b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java @@ -21,10 +21,13 @@ package org.isoron.uhabits.models; import java.util.*; +import javax.annotation.concurrent.*; + /** * A ModelObservable allows objects to subscribe themselves to it and receive * notifications whenever the model is changed. */ +@ThreadSafe public class ModelObservable { private List listeners; @@ -43,7 +46,7 @@ public class ModelObservable * * @param l the listener to be added. */ - public void addListener(Listener l) + public synchronized void addListener(Listener l) { listeners.add(l); } @@ -53,7 +56,7 @@ public class ModelObservable *

* Only models should call this method. */ - public void notifyListeners() + public synchronized void notifyListeners() { for (Listener l : listeners) l.onModelChange(); } @@ -66,7 +69,7 @@ public class ModelObservable * * @param l the listener to be removed */ - public void removeListener(Listener l) + public synchronized void removeListener(Listener l) { listeners.remove(l); } diff --git a/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java b/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java index e1626fe34..fac5849f4 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java @@ -60,7 +60,7 @@ public class AndroidTaskRunner implements TaskRunner public void execute(Task task) { task.onAttached(this); - new CustomAsyncTask(task).execute(); + new CustomAsyncTask(task).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override From 7bf9f88ee37fe5516a6a6e2bb23bd7761426cc92 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 23 Mar 2017 18:26:58 -0400 Subject: [PATCH 010/187] Implement NumberButtonView and related classes --- .../list/views/CheckmarkButtonViewTest.java | 93 ------- .../list/views/CheckmarkPanelViewTest.java | 2 +- .../habits/list/ListHabitsComponent.java | 10 +- .../controllers/NumberButtonController.java | 102 +++++++ .../list/views/CheckmarkButtonView.java | 24 +- .../habits/list/views/CheckmarkPanelView.java | 121 +++++---- .../habits/list/views/HabitCardView.java | 4 +- .../habits/list/views/NumberButtonView.java | 131 +++++++++ .../habits/list/views/NumberPanelView.java | 255 ++++++++++++++++++ .../uhabits/utils/AttributeSetUtils.java | 10 + .../res/layout/list_habits_button_preview.xml | 67 +++++ .../res/layout/list_habits_panel_preview.xml | 80 ++++++ 12 files changed, 748 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java create mode 100644 app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java create mode 100644 app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java create mode 100644 app/src/main/res/layout/list_habits_button_preview.xml create mode 100644 app/src/main/res/layout/list_habits_panel_preview.xml diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java index b8bbc0bac..e30669414 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java @@ -91,97 +91,4 @@ public class CheckmarkButtonViewTest extends BaseViewTest { assertRenders(view, PATH + "render_unchecked.png"); } - -// @Test -// public void testLongClick() throws Exception -// { -// setOnToggleListener(); -// view.performLongClick(); -// waitForLatch(); -// assertRendersCheckedExplicitly(); -// } -// -// @Test -// public void testClick_withShortToggle_fromUnchecked() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.UNCHECKED); -// setOnToggleListenerAndPerformClick(); -// assertRendersCheckedExplicitly(); -// } -// -// @Test -// public void testClick_withShortToggle_fromChecked() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.CHECKED_EXPLICITLY); -// setOnToggleListenerAndPerformClick(); -// assertRendersUnchecked(); -// } -// -// @Test -// public void testClick_withShortToggle_withoutListener() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(true); -// view.setValue(Checkmark.CHECKED_EXPLICITLY); -// view.setController(null); -// view.performClick(); -// assertRendersUnchecked(); -// } -// -// protected void setOnToggleListenerAndPerformClick() throws InterruptedException -// { -// setOnToggleListener(); -// view.performClick(); -// waitForLatch(); -// } -// -// @Test -// public void testClick_withoutShortToggle() throws Exception -// { -// Preferences.getInstance().setShortToggleEnabled(false); -// setOnInvalidToggleListener(); -// view.performClick(); -// waitForLatch(); -// assertRendersUnchecked(); -// } - -// protected void setOnInvalidToggleListener() -// { -// view.setController(new CheckmarkButtonView.Controller() -// { -// @Override -// public void onToggleCheckmark(CheckmarkButtonView view, long timestamp) -// { -// fail(); -// } -// -// @Override -// public void onInvalidToggle(CheckmarkButtonView v) -// { -// assertThat(v, equalTo(view)); -// latch.countDown(); -// } -// }); -// } - -// protected void setOnToggleListener() -// { -// view.setController(new CheckmarkButtonView.Controller() -// { -// @Override -// public void onToggleCheckmark(CheckmarkButtonView v, long t) -// { -// assertThat(v, equalTo(view)); -// assertThat(t, equalTo(DateUtils.getStartOfToday())); -// latch.countDown(); -// } -// -// @Override -// public void onInvalidToggle(CheckmarkButtonView view) -// { -// fail(); -// } -// }); -// } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java index 30e225a7a..8be616ef9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java @@ -59,7 +59,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest view = new CheckmarkPanelView(targetContext); view.setHabit(habit); - view.setCheckmarkValues(checkmarks); + view.setValues(checkmarks); view.setButtonCount(4); view.setColor(ColorUtils.getAndroidTestColor(7)); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java index 554c22a3b..4f8161d40 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java @@ -32,19 +32,21 @@ import dagger.*; dependencies = { AppComponent.class }) public interface ListHabitsComponent { - CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); - HabitCardListAdapter getAdapter(); + CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); + ListHabitsController getController(); ListHabitsMenu getMenu(); + MidnightTimer getMidnightTimer(); + + NumberButtonControllerFactory getNumberButtonControllerFactory(); + ListHabitsRootView getRootView(); ListHabitsScreen getScreen(); ListHabitsSelectionMenu getSelectionMenu(); - - MidnightTimer getMidnightTimer(); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java new file mode 100644 index 000000000..0cd31fe20 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/NumberButtonController.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 Á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.controllers; + +import android.support.annotation.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +@AutoFactory +public class NumberButtonController +{ + @Nullable + private NumberButtonView view; + + @Nullable + private Listener listener; + + @NonNull + private final Preferences prefs; + + @NonNull + private Habit habit; + + private long timestamp; + + public NumberButtonController(@Provided @NonNull Preferences prefs, + @NonNull Habit habit, + long timestamp) + { + this.habit = habit; + this.timestamp = timestamp; + this.prefs = prefs; + } + + public void onClick() + { + if (prefs.isShortToggleEnabled()) performEdit(); + else performInvalidToggle(); + } + + public boolean onLongClick() + { + performEdit(); + return true; + } + + public void performInvalidToggle() + { + if (listener != null) listener.onInvalidEdit(); + } + + public void performEdit() + { + if (listener != null) listener.onEdit(habit, timestamp); + } + + public void setListener(@Nullable Listener listener) + { + this.listener = listener; + } + + public void setView(@Nullable NumberButtonView view) + { + this.view = view; + } + + public interface Listener + { + /** + * Called when the user's attempt to edit the value is rejected. + */ + void onInvalidEdit(); + + /** + * Called when a the user's attempt to edit the value has been accepted. + * @param habit the habit being edited + * @param timestamp the timestamp being edited + */ + void onEdit(@NonNull Habit habit, long timestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java index 97d0c9fca..acc0e8b7e 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java @@ -20,6 +20,8 @@ package org.isoron.uhabits.activities.habits.list.views; import android.content.*; +import android.support.annotation.*; +import android.util.*; import android.view.*; import android.widget.*; @@ -28,6 +30,9 @@ import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.models.*; import org.isoron.uhabits.utils.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + public class CheckmarkButtonView extends TextView { private int color; @@ -36,16 +41,31 @@ public class CheckmarkButtonView extends TextView private StyledResources res; - public CheckmarkButtonView(Context context) + public CheckmarkButtonView(@Nullable Context context) { super(context); init(); } + public CheckmarkButtonView(@Nullable Context context, + @Nullable AttributeSet attrs) + { + super(context, attrs); + init(); + + if (context != null && attrs != null) + { + int color = getIntAttribute(context, attrs, "color", 0); + int value = getIntAttribute(context, attrs, "value", 0); + setColor(getAndroidTestColor(color)); + setValue(value); + } + } + public void setColor(int color) { this.color = color; - postInvalidate(); + updateText(); } public void setController(final CheckmarkButtonController controller) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java index b569f9208..1f206ad4b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java @@ -31,18 +31,23 @@ import org.isoron.uhabits.models.*; import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.utils.*; +import java.util.*; + import static android.view.View.MeasureSpec.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; -public class CheckmarkPanelView extends LinearLayout implements Preferences.Listener +public class CheckmarkPanelView extends LinearLayout + implements Preferences.Listener { - private static final int CHECKMARK_LEFT_TO_RIGHT = 0; + private static final int LEFT_TO_RIGHT = 0; - private static final int CHECKMARK_RIGHT_TO_LEFT = 1; + private static final int RIGHT_TO_LEFT = 1; @Nullable private Preferences prefs; - private int checkmarkValues[]; + private int values[]; private int nButtons; @@ -61,61 +66,89 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List init(); } - public CheckmarkPanelView(Context context, AttributeSet attrs) + public CheckmarkPanelView(Context ctx, AttributeSet attrs) { - super(context, attrs); + super(ctx, attrs); init(); + + if (ctx != null && attrs != null) + { + int paletteColor = getIntAttribute(ctx, attrs, "color", 0); + setColor(getAndroidTestColor(paletteColor)); + setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5)); + } + + if (isInEditMode()) initEditMode(); } public CheckmarkButtonView indexToButton(int i) { int position = i; - if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT) - position = nButtons - i - 1; + if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1; return (CheckmarkButtonView) getChildAt(position); } + @Override + public void onCheckmarkOrderChanged() + { + setupButtons(); + } + public void setButtonCount(int newButtonCount) { - if(nButtons != newButtonCount) + if (nButtons != newButtonCount) { nButtons = newButtonCount; - addCheckmarkButtons(); + addButtons(); } - setupCheckmarkButtons(); - } - - public void setCheckmarkValues(int[] checkmarkValues) - { - this.checkmarkValues = checkmarkValues; - setupCheckmarkButtons(); + setupButtons(); } public void setColor(int color) { this.color = color; - setupCheckmarkButtons(); + setupButtons(); } public void setController(Controller controller) { this.controller = controller; - setupCheckmarkButtons(); + setupButtons(); } public void setDataOffset(int dataOffset) { this.dataOffset = dataOffset; - setupCheckmarkButtons(); + setupButtons(); } public void setHabit(@NonNull Habit habit) { this.habit = habit; - setupCheckmarkButtons(); + setupButtons(); + } + + public void setValues(int[] values) + { + this.values = values; + setupButtons(); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if (prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); } @Override @@ -133,7 +166,7 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List super.onMeasure(widthSpec, heightSpec); } - private void addCheckmarkButtons() + private void addButtons() { removeAllViews(); @@ -143,21 +176,31 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List private int getCheckmarkOrder() { - if (prefs == null) return CHECKMARK_LEFT_TO_RIGHT; - return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT : - CHECKMARK_LEFT_TO_RIGHT; + if (prefs == null) return LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; } private void init() { Context appContext = getContext().getApplicationContext(); - if(appContext instanceof HabitsApplication) + if (appContext instanceof HabitsApplication) { HabitsApplication app = (HabitsApplication) appContext; prefs = app.getComponent().getPreferences(); } setWillNotDraw(false); + values = new int[0]; + } + + private void initEditMode() + { + int values[] = new int[nButtons]; + + for (int i = 0; i < nButtons; i++) + values[i] = new Random().nextInt(3); + + setValues(values); } private void setupButtonControllers(long timestamp, @@ -178,7 +221,7 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List buttonView.setController(buttonController); } - private void setupCheckmarkButtons() + private void setupButtons() { long timestamp = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; @@ -187,34 +230,14 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List for (int i = 0; i < nButtons; i++) { CheckmarkButtonView buttonView = indexToButton(i); - if(i + dataOffset >= checkmarkValues.length) break; - buttonView.setValue(checkmarkValues[i + dataOffset]); + if (i + dataOffset >= values.length) break; + buttonView.setValue(values[i + dataOffset]); buttonView.setColor(color); setupButtonControllers(timestamp, buttonView); timestamp -= day; } } - @Override - protected void onAttachedToWindow() - { - super.onAttachedToWindow(); - if(prefs != null) prefs.addListener(this); - } - - @Override - protected void onDetachedFromWindow() - { - if(prefs != null) prefs.removeListener(this); - super.onDetachedFromWindow(); - } - - @Override - public void onCheckmarkOrderChanged() - { - setupCheckmarkButtons(); - } - public interface Controller extends CheckmarkButtonController.Listener { diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index d57e0a71c..e703ba18c 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -99,7 +99,7 @@ public class HabitCardView extends FrameLayout public void setCheckmarkValues(int checkmarks[]) { - checkmarkPanel.setCheckmarkValues(checkmarks); + checkmarkPanel.setValues(checkmarks); postInvalidate(); } @@ -213,7 +213,7 @@ public class HabitCardView extends FrameLayout scoreRing.setColor(color); scoreRing.setPercentage(rand.nextFloat()); checkmarkPanel.setColor(color); - checkmarkPanel.setCheckmarkValues(values); + checkmarkPanel.setValues(values); } private void refresh() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java new file mode 100644 index 000000000..1d4d4f103 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2016 Á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 android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.utils.*; + +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + +public class NumberButtonView extends TextView +{ + private static Typeface TYPEFACE = + Typeface.create("sans-serif-condensed", Typeface.BOLD); + + private int color; + + private int value; + + private int threshold; + + private StyledResources res; + + public NumberButtonView(@Nullable Context context) + { + super(context); + init(); + } + + public NumberButtonView(@Nullable Context context, + @Nullable AttributeSet attrs) + { + super(context, attrs); + init(); + + if (context != null && attrs != null) + { + int color = getIntAttribute(context, attrs, "color", 0); + int value = getIntAttribute(context, attrs, "value", 0); + int threshold = getIntAttribute(context, attrs, "threshold", 1); + setColor(getAndroidTestColor(color)); + setThreshold(threshold); + setValue(value); + } + } + + private static String formatValue(int v) + { + double fv = (double) v; + if(v >= 1e9) return String.format("%.2fG", fv / 1e9); + if(v >= 1e8) return String.format("%.0fM", fv / 1e6); + if(v >= 1e7) return String.format("%.1fM", fv / 1e6); + if(v >= 1e6) return String.format("%.2fM", fv / 1e6); + if(v >= 1e5) return String.format("%.0fk", fv / 1e3); + if(v >= 1e4) return String.format("%.1fk", fv / 1e3); + if(v >= 1e3) return String.format("%.2fk", fv / 1e3); + return String.format("%d", v); + } + + public void setColor(int color) + { + this.color = color; + postInvalidate(); + } + + public void setController(final NumberButtonController controller) + { + setOnClickListener(v -> controller.onClick()); + setOnLongClickListener(v -> controller.onLongClick()); + } + + public void setThreshold(int threshold) + { + this.threshold = threshold; + updateText(); + } + + public void setValue(int value) + { + this.value = value; + updateText(); + } + + private void init() + { + res = new StyledResources(getContext()); + + setWillNotDraw(false); + + setMinHeight( + getResources().getDimensionPixelSize(R.dimen.checkmarkHeight)); + setMinWidth( + getResources().getDimensionPixelSize(R.dimen.checkmarkWidth)); + + setFocusable(false); + setGravity(Gravity.CENTER); + setTypeface(TYPEFACE); + } + + private void updateText() + { + int lowColor = res.getColor(R.attr.lowContrastTextColor); + setTextColor(value >= threshold ? color : lowColor); + setText(formatValue(value)); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java new file mode 100644 index 000000000..16ac11e9a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2016 Á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 android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import static android.view.View.MeasureSpec.*; +import static org.isoron.uhabits.utils.AttributeSetUtils.*; +import static org.isoron.uhabits.utils.ColorUtils.*; + +public class NumberPanelView extends LinearLayout + implements Preferences.Listener +{ + private static final int LEFT_TO_RIGHT = 0; + + private static final int RIGHT_TO_LEFT = 1; + + @Nullable + private Preferences prefs; + + private int values[]; + + private int threshold; + + private int nButtons; + + private int color; + + private Controller controller; + + @NonNull + private Habit habit; + + private int dataOffset; + + public NumberPanelView(Context context) + { + super(context); + init(); + } + + public NumberPanelView(Context ctx, AttributeSet attrs) + { + super(ctx, attrs); + init(); + + if (ctx != null && attrs != null) + { + int paletteColor = getIntAttribute(ctx, attrs, "color", 0); + setColor(getAndroidTestColor(paletteColor)); + setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5)); + setThreshold(getIntAttribute(ctx, attrs, "threshold", 1)); + } + + if(isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int values[] = new int[nButtons]; + + for(int i = 0; i < nButtons; i++) + values[i] = new Random().nextInt(threshold * 3); + + setValues(values); + } + + public NumberButtonView indexToButton(int i) + { + int position = i; + + if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1; + + return (NumberButtonView) getChildAt(position); + } + + @Override + public void onCheckmarkOrderChanged() + { + setupButtons(); + } + + public void setButtonCount(int newButtonCount) + { + if (nButtons != newButtonCount) + { + nButtons = newButtonCount; + addButtons(); + } + + setupButtons(); + } + + public void setColor(int color) + { + this.color = color; + setupButtons(); + } + + public void setController(Controller controller) + { + this.controller = controller; + setupButtons(); + } + + public void setDataOffset(int dataOffset) + { + this.dataOffset = dataOffset; + setupButtons(); + } + + public void setHabit(@NonNull Habit habit) + { + this.habit = habit; + setupButtons(); + } + + public void setThreshold(int threshold) + { + this.threshold = threshold; + setupButtons(); + } + + public void setValues(int[] values) + { + this.values = values; + setupButtons(); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if (prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) + { + float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth); + float buttonHeight = + getResources().getDimension(R.dimen.checkmarkHeight); + + float width = buttonWidth * nButtons; + + widthSpec = makeMeasureSpec((int) width, EXACTLY); + heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY); + + super.onMeasure(widthSpec, heightSpec); + } + + private void addButtons() + { + removeAllViews(); + + for (int i = 0; i < nButtons; i++) + addView(new NumberButtonView(getContext())); + } + + private int getCheckmarkOrder() + { + if (prefs == null) return LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + prefs = app.getComponent().getPreferences(); + } + + setWillNotDraw(false); + values = new int[0]; + } + + private void setupButtonControllers(long timestamp, + NumberButtonView buttonView) + { + if (controller == null) return; + if (!(getContext() instanceof ListHabitsActivity)) return; + + ListHabitsActivity activity = (ListHabitsActivity) getContext(); + NumberButtonControllerFactory buttonControllerFactory = activity + .getListHabitsComponent() + .getNumberButtonControllerFactory(); + + NumberButtonController buttonController = + buttonControllerFactory.create(habit, timestamp); + buttonController.setListener(controller); + buttonController.setView(buttonView); + buttonView.setController(buttonController); + } + + private void setupButtons() + { + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + timestamp -= day * dataOffset; + + for (int i = 0; i < nButtons; i++) + { + NumberButtonView buttonView = indexToButton(i); + if (i + dataOffset >= values.length) break; + buttonView.setValue(values[i + dataOffset]); + buttonView.setColor(color); + buttonView.setThreshold(threshold); + setupButtonControllers(timestamp, buttonView); + timestamp -= day; + } + } + + public interface Controller extends NumberButtonController.Listener + { + + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java index c633a9b72..67c9de531 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java @@ -74,4 +74,14 @@ public class AttributeSetUtils if (number != null) return Float.parseFloat(number); else return defaultValue; } + + public static int getIntAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + int defaultValue) + { + String number = getAttribute(context, attrs, name, null); + if (number != null) return Integer.parseInt(number); + else return defaultValue; + } } diff --git a/app/src/main/res/layout/list_habits_button_preview.xml b/app/src/main/res/layout/list_habits_button_preview.xml new file mode 100644 index 000000000..5f2724a07 --- /dev/null +++ b/app/src/main/res/layout/list_habits_button_preview.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_panel_preview.xml b/app/src/main/res/layout/list_habits_panel_preview.xml new file mode 100644 index 000000000..54393cad1 --- /dev/null +++ b/app/src/main/res/layout/list_habits_panel_preview.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e2a8de3acfac8e3bb6d4199fd8a0269389436152 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 23 Mar 2017 19:09:44 -0400 Subject: [PATCH 011/187] Update controllers and HabitCardView --- .../habits/list/views/HabitCardViewTest.java | 2 +- .../habits/list/ListHabitsController.java | 12 ++++++ .../list/controllers/HabitCardController.java | 20 +++++++-- .../controllers/HabitCardListController.java | 17 +++++++- .../habits/list/views/HabitCardListView.java | 4 +- .../habits/list/views/HabitCardView.java | 42 ++++++++++++++----- .../habits/list/views/NumberButtonView.java | 8 ++-- .../habits/list/views/NumberPanelView.java | 2 +- app/src/main/res/layout/list_habits_card.xml | 5 +++ app/src/main/res/values/strings.xml | 2 + 10 files changed, 91 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java index 2ca386e18..162f56b9b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java @@ -59,7 +59,7 @@ public class HabitCardViewTest extends BaseViewTest view = new HabitCardView(targetContext); view.setHabit(habit); - view.setCheckmarkValues(values); + view.setValues(values); view.setSelected(false); view.setScore(habit.getScores().getTodayValue()); view.setController(controller); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index a52c4b4d6..d0b584d05 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -157,6 +157,18 @@ public class ListHabitsController })); } + @Override + public void onInvalidEdit() + { + screen.showMessage(R.string.long_press_to_edit); + } + + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + + } + @Override public void onInvalidToggle() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java index 01e2b4643..aa29f37c3 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java @@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.controllers; import android.support.annotation.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.activities.habits.list.views.HabitCardView; +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; public class HabitCardController implements HabitCardView.Controller { @@ -32,6 +32,18 @@ public class HabitCardController implements HabitCardView.Controller @Nullable private Listener listener; + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + if(listener != null) listener.onEdit(habit, timestamp); + } + + @Override + public void onInvalidEdit() + { + if(listener != null) listener.onInvalidEdit(); + } + @Override public void onInvalidToggle() { @@ -55,7 +67,9 @@ public class HabitCardController implements HabitCardView.Controller this.view = view; } - public interface Listener extends CheckmarkButtonController.Listener + public interface Listener extends CheckmarkButtonController.Listener, + NumberButtonController.Listener { + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java index d710c3572..0d62048bc 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java @@ -21,9 +21,9 @@ package org.isoron.uhabits.activities.habits.list.controllers; import android.support.annotation.*; -import org.isoron.uhabits.models.*; import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; /** * Controller responsible for receiving and processing the events generated by a @@ -75,6 +75,18 @@ public class HabitCardListController implements HabitCardListView.Controller habitListener.onHabitReorder(habitFrom, habitTo); } + @Override + public void onEdit(@NonNull Habit habit, long timestamp) + { + if (habitListener != null) habitListener.onEdit(habit, timestamp); + } + + @Override + public void onInvalidEdit() + { + if (habitListener != null) habitListener.onInvalidEdit(); + } + /** * Called when the user attempts to perform a toggle, but attempt is * rejected. @@ -172,7 +184,8 @@ public class HabitCardListController implements HabitCardListView.Controller if (selectionListener != null) selectionListener.onSelectionFinish(); } - public interface HabitListener extends CheckmarkButtonController.Listener + public interface HabitListener extends CheckmarkButtonController.Listener, + NumberButtonController.Listener { /** * Called when the user clicks a habit. diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java index bc784465e..51e22c65d 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java @@ -91,8 +91,8 @@ public class HabitCardListView extends RecyclerView HabitCardView cardView = (HabitCardView) holder.itemView; cardView.setHabit(habit); cardView.setSelected(selected); - cardView.setCheckmarkValues(checkmarks); - cardView.setCheckmarkCount(checkmarkCount); + cardView.setValues(checkmarks); + cardView.setButtonCount(checkmarkCount); cardView.setDataOffset(dataOffset); cardView.setScore(score); if (controller != null) setupCardViewController(holder); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index e703ba18c..028da530c 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -56,6 +56,9 @@ public class HabitCardView extends FrameLayout @BindView(R.id.checkmarkPanel) CheckmarkPanelView checkmarkPanel; + @BindView(R.id.numberPanel) + NumberPanelView numberPanel; + @BindView(R.id.innerFrame) LinearLayout innerFrame; @@ -92,28 +95,38 @@ public class HabitCardView extends FrameLayout new Handler(Looper.getMainLooper()).post(() -> refresh()); } - public void setCheckmarkCount(int checkmarkCount) + public void setButtonCount(int buttonCount) { - checkmarkPanel.setButtonCount(checkmarkCount); + checkmarkPanel.setButtonCount(buttonCount); + numberPanel.setButtonCount(buttonCount); } - public void setCheckmarkValues(int checkmarks[]) + public void setValues(int values[]) { - checkmarkPanel.setValues(checkmarks); + checkmarkPanel.setValues(values); + + int[] magnitudes = new int[]{10, 100, 1000, 10000}; + int threshold = magnitudes[new Random().nextInt(4)]; + numberPanel.setThreshold(threshold); + numberPanel.initEditMode(); + postInvalidate(); } public void setController(Controller controller) { checkmarkPanel.setController(null); + numberPanel.setController(null); if (controller == null) return; checkmarkPanel.setController(controller); + numberPanel.setController(controller); } public void setDataOffset(int dataOffset) { this.dataOffset = dataOffset; checkmarkPanel.setDataOffset(dataOffset); + numberPanel.setDataOffset(dataOffset); } public void setHabit(@NonNull Habit habit) @@ -122,6 +135,7 @@ public class HabitCardView extends FrameLayout this.habit = habit; checkmarkPanel.setHabit(habit); + numberPanel.setHabit(habit); refresh(); attachToHabit(); @@ -191,7 +205,8 @@ public class HabitCardView extends FrameLayout inflate(context, R.layout.list_habits_card, this); ButterKnife.bind(this); - innerFrame.setOnTouchListener((v, event) -> { + innerFrame.setOnTouchListener((v, event) -> + { if (SDK_INT >= LOLLIPOP) v.getBackground().setHotspot(event.getX(), event.getY()); return false; @@ -205,15 +220,12 @@ public class HabitCardView extends FrameLayout { Random rand = new Random(); int color = ColorUtils.getAndroidTestColor(rand.nextInt(10)); - int[] values = new int[5]; - for (int i = 0; i < 5; i++) values[i] = rand.nextInt(3); - label.setText(EDIT_MODE_HABITS[rand.nextInt(EDIT_MODE_HABITS.length)]); label.setTextColor(color); scoreRing.setColor(color); scoreRing.setPercentage(rand.nextFloat()); checkmarkPanel.setColor(color); - checkmarkPanel.setValues(values); + numberPanel.setColor(color); } private void refresh() @@ -223,6 +235,12 @@ public class HabitCardView extends FrameLayout label.setTextColor(color); scoreRing.setColor(color); checkmarkPanel.setColor(color); + numberPanel.setColor(color); + + boolean isNumberHabit = false; //(new Random().nextInt(3) == 0); + checkmarkPanel.setVisibility(isNumberHabit ? GONE : VISIBLE); + numberPanel.setVisibility(isNumberHabit ? VISIBLE : GONE); + postInvalidate(); } @@ -256,5 +274,9 @@ public class HabitCardView extends FrameLayout } } - public interface Controller extends CheckmarkPanelView.Controller {} + public interface Controller + extends CheckmarkPanelView.Controller, NumberPanelView.Controller + { + + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java index 1d4d4f103..ff4e622d6 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.java @@ -36,7 +36,7 @@ import static org.isoron.uhabits.utils.ColorUtils.*; public class NumberButtonView extends TextView { private static Typeface TYPEFACE = - Typeface.create("sans-serif-condensed", Typeface.BOLD); + Typeface.create("sans-serif-condensed", Typeface.NORMAL); private int color; @@ -72,13 +72,13 @@ public class NumberButtonView extends TextView private static String formatValue(int v) { double fv = (double) v; - if(v >= 1e9) return String.format("%.2fG", fv / 1e9); + if(v >= 1e9) return String.format("%.1fG", fv / 1e9); if(v >= 1e8) return String.format("%.0fM", fv / 1e6); if(v >= 1e7) return String.format("%.1fM", fv / 1e6); - if(v >= 1e6) return String.format("%.2fM", fv / 1e6); + if(v >= 1e6) return String.format("%.1fM", fv / 1e6); if(v >= 1e5) return String.format("%.0fk", fv / 1e3); if(v >= 1e4) return String.format("%.1fk", fv / 1e3); - if(v >= 1e3) return String.format("%.2fk", fv / 1e3); + if(v >= 1e3) return String.format("%.1fk", fv / 1e3); return String.format("%d", v); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java index 16ac11e9a..cfbf3de15 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.java @@ -84,7 +84,7 @@ public class NumberPanelView extends LinearLayout if(isInEditMode()) initEditMode(); } - private void initEditMode() + public void initEditMode() { int values[] = new int[nButtons]; diff --git a/app/src/main/res/layout/list_habits_card.xml b/app/src/main/res/layout/list_habits_card.xml index 7b8d0f54d..6109c7472 100644 --- a/app/src/main/res/layout/list_habits_card.xml +++ b/app/src/main/res/layout/list_habits_card.xml @@ -46,6 +46,11 @@ android:id="@+id/checkmarkPanel" style="@style/ListHabits.CheckmarkPanel"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e94bea9a..a28e7e576 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,4 +204,6 @@ By score Download Export + Press-and-hold to change the + value \ No newline at end of file From ac32460859b57beecbd8482c4232a7cd7c1aba97 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 24 Mar 2017 08:20:59 -0400 Subject: [PATCH 012/187] Create number picker dialog --- .../habits/list/ListHabitsController.java | 8 +- .../habits/list/ListHabitsScreen.java | 177 +++++++++++------- .../main/res/layout/number_picker_dialog.xml | 33 ++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 app/src/main/res/layout/number_picker_dialog.xml diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index d0b584d05..08be54b39 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -20,6 +20,7 @@ package org.isoron.uhabits.activities.habits.list; import android.support.annotation.*; +import android.util.*; import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; @@ -166,7 +167,12 @@ public class ListHabitsController @Override public void onEdit(@NonNull Habit habit, long timestamp) { - + int oldValue = habit.getCheckmarks().getTodayValue(); + screen.showNumberPicker(oldValue, newValue -> { + Log.d("ListHabitsController", + String.format("%s %d %d", habit.getName(), timestamp, + newValue)); + }); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 045505a90..822964d7b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -23,6 +23,9 @@ import android.app.*; import android.content.*; import android.net.*; import android.support.annotation.*; +import android.support.v7.app.AlertDialog; +import android.view.*; +import android.widget.*; import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; @@ -41,25 +44,26 @@ import javax.inject.*; import static android.os.Build.VERSION.*; import static android.os.Build.VERSION_CODES.*; +import static org.isoron.uhabits.utils.InterfaceUtils.*; @ActivityScope public class ListHabitsScreen extends BaseScreen implements CommandRunner.Listener { - public static final int RESULT_IMPORT_DATA = 1; + public static final int REQUEST_OPEN_DOCUMENT = 6; + + public static final int REQUEST_SETTINGS = 7; + + public static final int RESULT_BUG_REPORT = 4; public static final int RESULT_EXPORT_CSV = 2; public static final int RESULT_EXPORT_DB = 3; - public static final int RESULT_BUG_REPORT = 4; + public static final int RESULT_IMPORT_DATA = 1; public static final int RESULT_REPAIR_DB = 5; - public static final int REQUEST_OPEN_DOCUMENT = 6; - - public static final int REQUEST_SETTINGS = 7; - @Nullable private ListHabitsController controller; @@ -97,11 +101,16 @@ public class ListHabitsScreen extends BaseScreen @NonNull ListHabitsRootView rootView, @NonNull IntentFactory intentFactory, @NonNull ThemeSwitcher themeSwitcher, - @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, - @NonNull CreateHabitDialogFactory createHabitDialogFactory, - @NonNull FilePickerDialogFactory filePickerDialogFactory, - @NonNull ColorPickerDialogFactory colorPickerFactory, - @NonNull EditHabitDialogFactory editHabitDialogFactory) + @NonNull + ConfirmDeleteDialogFactory confirmDeleteDialogFactory, + @NonNull + CreateHabitDialogFactory createHabitDialogFactory, + @NonNull + FilePickerDialogFactory filePickerDialogFactory, + @NonNull + ColorPickerDialogFactory colorPickerFactory, + @NonNull + EditHabitDialogFactory editHabitDialogFactory) { super(activity); setRootView(rootView); @@ -139,60 +148,7 @@ public class ListHabitsScreen extends BaseScreen if (requestCode == REQUEST_OPEN_DOCUMENT) onOpenDocumentResult(resultCode, data); - if (requestCode == REQUEST_SETTINGS) - onSettingsResult(resultCode); - } - - private void onSettingsResult(int resultCode) - { - if (controller == null) return; - - switch (resultCode) - { - case RESULT_IMPORT_DATA: - showImportScreen(); - break; - - case RESULT_EXPORT_CSV: - controller.onExportCSV(); - break; - - case RESULT_EXPORT_DB: - controller.onExportDB(); - break; - - case RESULT_BUG_REPORT: - controller.onSendBugReport(); - break; - - case RESULT_REPAIR_DB: - controller.onRepairDB(); - break; - } - } - - private void onOpenDocumentResult(int resultCode, Intent data) - { - if (controller == null) return; - if (resultCode != Activity.RESULT_OK) return; - - try - { - Uri uri = data.getData(); - ContentResolver cr = activity.getContentResolver(); - InputStream is = cr.openInputStream(uri); - - File cacheDir = activity.getExternalCacheDir(); - File tempFile = File.createTempFile("import", "", cacheDir); - - FileUtils.copy(is, tempFile); - controller.onImportData(tempFile, () -> tempFile.delete()); - } - catch (IOException e) - { - showMessage(R.string.could_not_import); - e.printStackTrace(); - } + if (requestCode == REQUEST_SETTINGS) onSettingsResult(resultCode); } public void setController(@Nullable ListHabitsController controller) @@ -238,6 +194,36 @@ public class ListHabitsScreen extends BaseScreen activity.showDialog(dialog, "editHabit"); } + public void showNumberPicker(int initialValue, + @NonNull NumberPickerCallback callback) + { + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.number_picker_dialog, null); + + final NumberPicker picker = + (NumberPicker) view.findViewById(R.id.picker); + + picker.setMinValue(0); + picker.setMaxValue(Integer.MAX_VALUE); + picker.setValue(initialValue); + picker.setWrapSelectorWheel(false); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder + .setView(view) + .setTitle(R.string.change_value) + .setPositiveButton(android.R.string.ok, (dialog, which) -> + { + callback.onNumberPicked(picker.getValue()); + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + int width = (int) dpToPixels(activity, 200); + int height = (int) dpToPixels(activity, 275); + dialog.getWindow().setLayout(width, height); + } + public void showFAQScreen() { Intent intent = intentFactory.viewFAQ(activity); @@ -278,7 +264,9 @@ public class ListHabitsScreen extends BaseScreen FilePickerDialog picker = filePickerDialogFactory.create(dir); if (controller != null) - picker.setListener(file -> controller.onImportData(file, () -> {})); + picker.setListener(file -> controller.onImportData(file, () -> + { + })); activity.showDialog(picker.getDialog()); } @@ -300,4 +288,61 @@ public class ListHabitsScreen extends BaseScreen themeSwitcher.toggleNightMode(); activity.restartWithFade(); } + + private void onOpenDocumentResult(int resultCode, Intent data) + { + if (controller == null) return; + if (resultCode != Activity.RESULT_OK) return; + + try + { + Uri uri = data.getData(); + ContentResolver cr = activity.getContentResolver(); + InputStream is = cr.openInputStream(uri); + + File cacheDir = activity.getExternalCacheDir(); + File tempFile = File.createTempFile("import", "", cacheDir); + + FileUtils.copy(is, tempFile); + controller.onImportData(tempFile, () -> tempFile.delete()); + } + catch (IOException e) + { + showMessage(R.string.could_not_import); + e.printStackTrace(); + } + } + + private void onSettingsResult(int resultCode) + { + if (controller == null) return; + + switch (resultCode) + { + case RESULT_IMPORT_DATA: + showImportScreen(); + break; + + case RESULT_EXPORT_CSV: + controller.onExportCSV(); + break; + + case RESULT_EXPORT_DB: + controller.onExportDB(); + break; + + case RESULT_BUG_REPORT: + controller.onSendBugReport(); + break; + + case RESULT_REPAIR_DB: + controller.onRepairDB(); + break; + } + } + + public interface NumberPickerCallback + { + void onNumberPicked(int newValue); + } } diff --git a/app/src/main/res/layout/number_picker_dialog.xml b/app/src/main/res/layout/number_picker_dialog.xml new file mode 100644 index 000000000..396f9c819 --- /dev/null +++ b/app/src/main/res/layout/number_picker_dialog.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a28e7e576..0933fdf6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,4 +206,5 @@ Export Press-and-hold to change the value + Change value \ No newline at end of file From 5b9e90fe7ac92f884ce90a31b3da9b4abccef171 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 24 Mar 2017 09:41:52 -0400 Subject: [PATCH 013/187] Persist repetition values --- app/build.gradle | 2 +- .../sqlite/SQLiteRepetitionListTest.java | 3 +- app/src/main/assets/migrations/16.sql | 2 + .../activities/common/views/HistoryChart.java | 6 +- .../habits/list/ListHabitsController.java | 7 +- .../habits/list/views/HabitCardView.java | 12 +-- .../commands/CreateRepetitionCommand.java | 78 +++++++++++++++++++ .../isoron/uhabits/models/CheckmarkList.java | 11 ++- .../java/org/isoron/uhabits/models/Habit.java | 7 ++ .../org/isoron/uhabits/models/Repetition.java | 34 +++++++- .../isoron/uhabits/models/RepetitionList.java | 6 +- .../models/memory/MemoryRepetitionList.java | 3 - .../models/sqlite/SQLiteRepetitionList.java | 9 +-- .../sqlite/records/RepetitionRecord.java | 7 +- .../commands/CreateRepetitionCommandTest.java | 70 +++++++++++++++++ 15 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 app/src/main/assets/migrations/16.sql create mode 100644 app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java create mode 100644 app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java diff --git a/app/build.gradle b/app/build.gradle index 93d60fe16..649ab0f05 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdkVersion 15 targetSdkVersion 25 - buildConfigField "Integer", "databaseVersion", "15" + buildConfigField "Integer", "databaseVersion", "16" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java index 992c1e673..766271ada 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java @@ -37,6 +37,7 @@ import java.util.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; import static org.hamcrest.core.IsNot.not; +import static org.isoron.uhabits.models.Checkmark.*; @RunWith(AndroidJUnit4.class) @MediumTest @@ -67,7 +68,7 @@ public class SQLiteRepetitionListTest extends BaseAndroidTest RepetitionRecord record = getByTimestamp(today + day); assertThat(record, is(nullValue())); - Repetition rep = new Repetition(today + day); + Repetition rep = new Repetition(today + day, CHECKED_EXPLICITLY); habit.getRepetitions().add(rep); record = getByTimestamp(today + day); diff --git a/app/src/main/assets/migrations/16.sql b/app/src/main/assets/migrations/16.sql new file mode 100644 index 000000000..7a7746f63 --- /dev/null +++ b/app/src/main/assets/migrations/16.sql @@ -0,0 +1,2 @@ +alter table Habits add column type integer not null default 0; +alter table Repetitions add column value integer not null default 2; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java index 620a2d59f..798ecdb88 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java @@ -323,7 +323,11 @@ public class HistoryChart extends ScrollableChart int checkmarkOffset) { if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); - else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]); + else + { + int checkmark = checkmarks[checkmarkOffset]; + pSquareBg.setColor(colors[Integer.min(2, checkmark)]); + } pSquareFg.setColor(reverseTextColor); canvas.drawRect(location, pSquareBg); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index 08be54b39..7d02974e4 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -20,7 +20,6 @@ package org.isoron.uhabits.activities.habits.list; import android.support.annotation.*; -import android.util.*; import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; @@ -169,9 +168,9 @@ public class ListHabitsController { int oldValue = habit.getCheckmarks().getTodayValue(); screen.showNumberPicker(oldValue, newValue -> { - Log.d("ListHabitsController", - String.format("%s %d %d", habit.getName(), timestamp, - newValue)); + commandRunner.execute( + new CreateRepetitionCommand(habit, timestamp, newValue), + habit.getId()); }); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index 028da530c..febbfde84 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -104,11 +104,13 @@ public class HabitCardView extends FrameLayout public void setValues(int values[]) { checkmarkPanel.setValues(values); + numberPanel.setValues(values); + numberPanel.setThreshold(10); - int[] magnitudes = new int[]{10, 100, 1000, 10000}; - int threshold = magnitudes[new Random().nextInt(4)]; - numberPanel.setThreshold(threshold); - numberPanel.initEditMode(); +// int[] magnitudes = new int[]{10, 100, 1000, 10000}; +// int threshold = magnitudes[new Random().nextInt(4)]; +// numberPanel.setThreshold(threshold); +// numberPanel.initEditMode(); postInvalidate(); } @@ -237,7 +239,7 @@ public class HabitCardView extends FrameLayout checkmarkPanel.setColor(color); numberPanel.setColor(color); - boolean isNumberHabit = false; //(new Random().nextInt(3) == 0); + boolean isNumberHabit = true; //(new Random().nextInt(3) == 0); checkmarkPanel.setVisibility(isNumberHabit ? GONE : VISIBLE); numberPanel.setVisibility(isNumberHabit ? VISIBLE : GONE); diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java new file mode 100644 index 000000000..5422d7f1b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateRepetitionCommand.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Á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.commands; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +/** + * Command to toggle a repetition. + */ +public class CreateRepetitionCommand extends Command +{ + @NonNull + private final Habit habit; + + private final long timestamp; + + private final int value; + + private Repetition previousRep; + + private Repetition newRep; + + public CreateRepetitionCommand(@NonNull Habit habit, + long timestamp, + int value) + { + this.timestamp = timestamp; + this.habit = habit; + this.value = value; + } + + @Override + public void execute() + { + RepetitionList reps = habit.getRepetitions(); + + previousRep = reps.getByTimestamp(timestamp); + if (previousRep != null) reps.remove(previousRep); + + newRep = new Repetition(timestamp, value); + reps.add(newRep); + + habit.invalidateNewerThan(timestamp); + } + + @NonNull + public Habit getHabit() + { + return habit; + } + + @Override + public void undo() + { + habit.getRepetitions().remove(newRep); + if (previousRep != null) habit.getRepetitions().add(previousRep); + habit.invalidateNewerThan(timestamp); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 86488a8d1..5e71eb9e5 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -27,6 +27,9 @@ import java.io.*; import java.text.*; import java.util.*; +import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; +import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY; + /** * The collection of {@link Checkmark}s belonging to a habit. */ @@ -239,7 +242,7 @@ public abstract class CheckmarkList for (Repetition rep : reps) { int offset = (int) ((rep.getTimestamp() - fromExtended) / day); - checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY; + checks[nDaysExtended - offset - 1] = rep.getValue(); } for (int i = 0; i < nDays; i++) @@ -247,11 +250,11 @@ public abstract class CheckmarkList int counter = 0; for (int j = 0; j < freq.getDenominator(); j++) - if (checks[i + j] == 2) counter++; + if (checks[i + j] == CHECKED_EXPLICITLY) counter++; if (counter >= freq.getNumerator()) - if (checks[i] != Checkmark.CHECKED_EXPLICITLY) - checks[i] = Checkmark.CHECKED_IMPLICITLY; + if (checks[i] != CHECKED_EXPLICITLY) + checks[i] = CHECKED_IMPLICITLY; } List checkmarks = new LinkedList<>(); diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 5ba711407..5d81ddd64 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -274,4 +274,11 @@ public class Habit .append("archived", archived) .toString(); } + + public void invalidateNewerThan(long timestamp) + { + getScores().invalidateNewerThan(timestamp); + getCheckmarks().invalidateNewerThan(timestamp); + getStreaks().invalidateNewerThan(timestamp); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Repetition.java b/app/src/main/java/org/isoron/uhabits/models/Repetition.java index 72e378205..3104fda9c 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Repetition.java +++ b/app/src/main/java/org/isoron/uhabits/models/Repetition.java @@ -30,6 +30,8 @@ public final class Repetition private final long timestamp; + private final int value; + /** * Creates a new repetition with given parameters. *

@@ -38,9 +40,24 @@ public final class Repetition * * @param timestamp the time this repetition occurred. */ - public Repetition(long timestamp) + public Repetition(long timestamp, int value) { this.timestamp = timestamp; + this.value = value; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Repetition that = (Repetition) o; + + return new EqualsBuilder() + .append(timestamp, that.timestamp) + .append(value, that.value) + .isEquals(); } public long getTimestamp() @@ -48,11 +65,26 @@ public final class Repetition return timestamp; } + public int getValue() + { + return value; + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37) + .append(timestamp) + .append(value) + .toHashCode(); + } + @Override public String toString() { return new ToStringBuilder(this) .append("timestamp", timestamp) + .append("value", value) .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 07fa7b681..ec355267c 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -192,13 +192,11 @@ public abstract class RepetitionList if (rep != null) remove(rep); else { - rep = new Repetition(timestamp); + rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY); add(rep); } - habit.getScores().invalidateNewerThan(timestamp); - habit.getCheckmarks().invalidateNewerThan(timestamp); - habit.getStreaks().invalidateNewerThan(timestamp); + habit.invalidateNewerThan(timestamp); return rep; } diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java index dbac82b40..cf1772e0b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java @@ -86,7 +86,6 @@ public class MemoryRepetitionList extends RepetitionList oldestRep = rep; oldestTime = rep.getTimestamp(); } - } return oldestRep; @@ -106,7 +105,6 @@ public class MemoryRepetitionList extends RepetitionList newestRep = rep; newestTime = rep.getTimestamp(); } - } return newestRep; @@ -119,7 +117,6 @@ public class MemoryRepetitionList extends RepetitionList observable.notifyListeners(); } - @NonNull @Override public long getTotalCount() { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java index 6278863e9..2794950f5 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java @@ -73,7 +73,7 @@ public class SQLiteRepetitionList extends RepetitionList public List getByInterval(long timeFrom, long timeTo) { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? and timestamp >= ? and timestamp <= ? " + "order by timestamp"; @@ -93,7 +93,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getByTimestamp(long timestamp) { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? and timestamp = ? " + "limit 1"; @@ -111,7 +111,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getOldest() { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? " + "order by timestamp asc " + @@ -129,7 +129,7 @@ public class SQLiteRepetitionList extends RepetitionList public Repetition getNewest() { check(habit.getId()); - String query = "select habit, timestamp " + + String query = "select habit, timestamp, value " + "from Repetitions " + "where habit = ? " + "order by timestamp desc " + @@ -182,7 +182,6 @@ public class SQLiteRepetitionList extends RepetitionList return reps; } - @NonNull @Override public long getTotalCount() { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java index 5f831495f..874fd0954 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java @@ -38,6 +38,9 @@ public class RepetitionRecord extends Model implements SQLiteRecord @Column(name = "timestamp") public Long timestamp; + @Column(name = "value") + public int value; + public static RepetitionRecord get(Long id) { return RepetitionRecord.load(RepetitionRecord.class, id); @@ -46,16 +49,18 @@ public class RepetitionRecord extends Model implements SQLiteRecord public void copyFrom(Repetition repetition) { timestamp = repetition.getTimestamp(); + value = repetition.getValue(); } @Override public void copyFrom(Cursor c) { timestamp = c.getLong(1); + value = c.getInt(2); } public Repetition toRepetition() { - return new Repetition(timestamp); + return new Repetition(timestamp, value); } } diff --git a/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java b/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java new file mode 100644 index 000000000..45816c559 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/commands/CreateRepetitionCommandTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Á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.commands; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; + +import static junit.framework.Assert.*; +import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; + +public class CreateRepetitionCommandTest extends BaseUnitTest +{ + + private CreateRepetitionCommand command; + + private Habit habit; + + private long today; + + @Override + @Before + public void setUp() + { + super.setUp(); + + habit = fixtures.createShortHabit(); + + today = DateUtils.getStartOfToday(); + command = new CreateRepetitionCommand(habit, today, 100); + } + + @Test + public void testExecuteUndoRedo() + { + RepetitionList reps = habit.getRepetitions(); + + Repetition rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(CHECKED_EXPLICITLY, rep.getValue()); + + command.execute(); + rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(100, rep.getValue()); + + command.undo(); + rep = reps.getByTimestamp(today); + assertNotNull(rep); + assertEquals(CHECKED_EXPLICITLY, rep.getValue()); + } +} From 5c1ccfe6fe7f0f477913f1eb0df0fe64a29fa680 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 24 Mar 2017 10:13:19 -0400 Subject: [PATCH 014/187] Fix small issues with the number picker --- .../habits/list/ListHabitsController.java | 2 +- .../habits/list/ListHabitsScreen.java | 29 +++++++++++++------ .../isoron/uhabits/utils/InterfaceUtils.java | 22 ++++++++++++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index 7d02974e4..db0b8cbf5 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -166,7 +166,7 @@ public class ListHabitsController @Override public void onEdit(@NonNull Habit habit, long timestamp) { - int oldValue = habit.getCheckmarks().getTodayValue(); + int oldValue = habit.getCheckmarks().getValues(timestamp, timestamp)[0]; screen.showNumberPicker(oldValue, newValue -> { commandRunner.execute( new CreateRepetitionCommand(habit, timestamp, newValue), diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 822964d7b..486892cc7 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -42,8 +42,10 @@ import java.io.*; import javax.inject.*; +import static android.content.DialogInterface.*; import static android.os.Build.VERSION.*; import static android.os.Build.VERSION_CODES.*; +import static android.view.inputmethod.EditorInfo.*; import static org.isoron.uhabits.utils.InterfaceUtils.*; @ActivityScope @@ -208,20 +210,29 @@ public class ListHabitsScreen extends BaseScreen picker.setValue(initialValue); picker.setWrapSelectorWheel(false); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder + AlertDialog dialog = new AlertDialog.Builder(activity) .setView(view) .setTitle(R.string.change_value) - .setPositiveButton(android.R.string.ok, (dialog, which) -> - { + .setPositiveButton(android.R.string.ok, (d, which) -> { + picker.clearFocus(); callback.onNumberPicked(picker.getValue()); - }); + }).create(); + + InterfaceUtils.setupEditorAction(picker, (v, actionId, event) -> { + if (actionId == IME_ACTION_DONE) + dialog.getButton(BUTTON_POSITIVE).performClick(); + return false; + }); - AlertDialog dialog = builder.create(); dialog.show(); - int width = (int) dpToPixels(activity, 200); - int height = (int) dpToPixels(activity, 275); - dialog.getWindow().setLayout(width, height); + + Window window = dialog.getWindow(); + if (window != null) + { + int width = (int) dpToPixels(activity, 200); + int height = (int) dpToPixels(activity, 275); + window.setLayout(width, height); + } } public void showFAQScreen() diff --git a/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java index 5db2a875a..2bf26c040 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java @@ -22,7 +22,10 @@ package org.isoron.uhabits.utils; import android.content.*; import android.content.res.*; import android.graphics.*; +import android.support.annotation.*; import android.util.*; +import android.view.*; +import android.widget.*; import java.util.*; @@ -39,8 +42,9 @@ public abstract class InterfaceUtils public static Typeface getFontAwesome(Context context) { - if(fontAwesome == null) - fontAwesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf"); + if(fontAwesome == null) fontAwesome = + Typeface.createFromAsset(context.getAssets(), + "fontawesome-webfont.ttf"); return fontAwesome; } @@ -69,4 +73,18 @@ public abstract class InterfaceUtils return false; } + public static void setupEditorAction(@NonNull ViewGroup parent, + @NonNull TextView.OnEditorActionListener listener) + { + for (int i = 0; i < parent.getChildCount(); i++) + { + View child = parent.getChildAt(i); + + if (child instanceof ViewGroup) + setupEditorAction((ViewGroup) child, listener); + + if (child instanceof TextView) + ((TextView) child).setOnEditorActionListener(listener); + } + } } From d03edf28950fb972dac8b8b29db70d24caa4ecaa Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 24 Mar 2017 16:47:40 -0400 Subject: [PATCH 015/187] Update habit creation dialogs --- .../habits/edit/BaseDialogHelper.java | 69 +++-- ...aseDialog.java => BooleanHabitDialog.java} | 13 +- ...log.java => CreateBooleanHabitDialog.java} | 2 +- .../edit/CreateNumericalHabitDialog.java | 55 ++++ ...ialog.java => EditBooleanHabitDialog.java} | 2 +- ...ava => EditBooleanHabitDialogFactory.java} | 8 +- .../habits/edit/NumericalHabitDialog.java | 260 ++++++++++++++++++ .../habits/list/ListHabitsScreen.java | 134 +++++---- .../habits/list/views/HabitCardView.java | 12 +- .../habits/show/ShowHabitScreen.java | 9 +- .../java/org/isoron/uhabits/models/Habit.java | 41 ++- .../models/sqlite/records/HabitRecord.java | 11 +- ...{edit_habit.xml => edit_boolean_habit.xml} | 2 +- .../main/res/layout/edit_numerical_habit.xml | 110 ++++++++ app/src/main/res/values/constants.xml | 5 + app/src/main/res/values/styles.xml | 4 + .../habits/list/ListHabitsScreenTest.java | 18 +- 17 files changed, 635 insertions(+), 120 deletions(-) rename app/src/main/java/org/isoron/uhabits/activities/habits/edit/{BaseDialog.java => BooleanHabitDialog.java} (96%) rename app/src/main/java/org/isoron/uhabits/activities/habits/edit/{CreateHabitDialog.java => CreateBooleanHabitDialog.java} (96%) create mode 100644 app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateNumericalHabitDialog.java rename app/src/main/java/org/isoron/uhabits/activities/habits/edit/{EditHabitDialog.java => EditBooleanHabitDialog.java} (96%) rename app/src/main/java/org/isoron/uhabits/activities/habits/edit/{EditHabitDialogFactory.java => EditBooleanHabitDialogFactory.java} (84%) create mode 100644 app/src/main/java/org/isoron/uhabits/activities/habits/edit/NumericalHabitDialog.java rename app/src/main/res/layout/{edit_habit.xml => edit_boolean_habit.xml} (98%) create mode 100644 app/src/main/res/layout/edit_numerical_habit.xml diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java index 225b509ff..4712c8961 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java @@ -20,6 +20,7 @@ package org.isoron.uhabits.activities.habits.edit; import android.annotation.*; +import android.support.annotation.*; import android.support.v4.app.*; import android.view.*; import android.widget.*; @@ -40,9 +41,11 @@ public class BaseDialogHelper @BindView(R.id.tvDescription) TextView tvDescription; + @Nullable @BindView(R.id.tvFreqNum) TextView tvFreqNum; + @Nullable @BindView(R.id.tvFreqDen) TextView tvFreqDen; @@ -52,9 +55,11 @@ public class BaseDialogHelper @BindView(R.id.tvReminderDays) TextView tvReminderDays; + @Nullable @BindView(R.id.sFrequency) Spinner sFrequency; + @Nullable @BindView(R.id.llCustomFrequency) ViewGroup llCustomFrequency; @@ -69,9 +74,8 @@ public class BaseDialogHelper protected void populateForm(final Habit habit) { - if (habit.getName() != null) tvName.setText(habit.getName()); - if (habit.getDescription() != null) - tvDescription.setText(habit.getDescription()); + tvName.setText(habit.getName()); + tvDescription.setText(habit.getDescription()); populateColor(habit.getColor()); populateFrequencyFields(habit); @@ -82,13 +86,17 @@ public class BaseDialogHelper { habit.setName(tvName.getText().toString().trim()); habit.setDescription(tvDescription.getText().toString().trim()); - String freqNum = tvFreqNum.getText().toString(); - String freqDen = tvFreqDen.getText().toString(); - if (!freqNum.isEmpty() && !freqDen.isEmpty()) + + if (tvFreqDen != null && tvFreqNum != null) { - int numerator = Integer.parseInt(freqNum); - int denominator = Integer.parseInt(freqDen); - habit.setFrequency(new Frequency(numerator, denominator)); + String freqNum = tvFreqNum.getText().toString(); + String freqDen = tvFreqDen.getText().toString(); + if (!freqNum.isEmpty() && !freqDen.isEmpty()) + { + int numerator = Integer.parseInt(freqNum); + int denominator = Integer.parseInt(freqDen); + habit.setFrequency(new Frequency(numerator, denominator)); + } } } @@ -101,15 +109,16 @@ public class BaseDialogHelper @SuppressLint("SetTextI18n") void populateFrequencyFields(Habit habit) { + if (tvFreqNum == null) return; + if (tvFreqDen == null) return; + int quickSelectPosition = -1; Frequency freq = habit.getFrequency(); - if (freq.equals(Frequency.DAILY)) - quickSelectPosition = 0; + if (freq.equals(Frequency.DAILY)) quickSelectPosition = 0; - else if (freq.equals(Frequency.WEEKLY)) - quickSelectPosition = 1; + else if (freq.equals(Frequency.WEEKLY)) quickSelectPosition = 1; else if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) quickSelectPosition = 2; @@ -144,13 +153,16 @@ public class BaseDialogHelper tvReminderTime.setText(time); llReminderDays.setVisibility(View.VISIBLE); - boolean weekdays[] = reminder.getDays().toArray(); + boolean weekdays[] = reminder.getDays().toArray(); tvReminderDays.setText( DateUtils.formatWeekdayList(frag.getContext(), weekdays)); } private void showCustomFrequency() { + if(sFrequency == null) return; + if(llCustomFrequency == null) return; + sFrequency.setVisibility(View.GONE); llCustomFrequency.setVisibility(View.VISIBLE); } @@ -158,6 +170,9 @@ public class BaseDialogHelper @SuppressLint("SetTextI18n") private void showSimplifiedFrequency(int quickSelectPosition) { + if(sFrequency == null) return; + if(llCustomFrequency == null) return; + sFrequency.setVisibility(View.VISIBLE); sFrequency.setSelection(quickSelectPosition); llCustomFrequency.setVisibility(View.GONE); @@ -175,19 +190,21 @@ public class BaseDialogHelper } Frequency freq = habit.getFrequency(); - - if (freq.getNumerator() <= 0) + if (tvFreqNum != null && tvFreqDen != null) { - tvFreqNum.setError( - frag.getString(R.string.validation_number_should_be_positive)); - valid = false; - } - - if (freq.getNumerator() > freq.getDenominator()) - { - tvFreqNum.setError( - frag.getString(R.string.validation_at_most_one_rep_per_day)); - valid = false; + if (freq.getNumerator() <= 0) + { + tvFreqNum.setError(frag.getString( + R.string.validation_number_should_be_positive)); + valid = false; + } + + if (freq.getNumerator() > freq.getDenominator()) + { + tvFreqNum.setError(frag.getString( + R.string.validation_at_most_one_rep_per_day)); + valid = false; + } } return valid; diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BooleanHabitDialog.java similarity index 96% rename from app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java rename to app/src/main/java/org/isoron/uhabits/activities/habits/edit/BooleanHabitDialog.java index b4c228876..a2751ddc2 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BooleanHabitDialog.java @@ -39,7 +39,7 @@ import java.util.*; import butterknife.*; -public abstract class BaseDialog extends AppCompatDialogFragment +public abstract class BooleanHabitDialog extends AppCompatDialogFragment { @Nullable protected Habit originalHabit; @@ -62,6 +62,12 @@ public abstract class BaseDialog extends AppCompatDialogFragment private ColorPickerDialogFactory colorPickerDialogFactory; + @Override + public int getTheme() + { + return R.style.DialogWithTitle; + } + @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -77,7 +83,7 @@ public abstract class BaseDialog extends AppCompatDialogFragment ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.edit_habit, container, false); + View view = inflater.inflate(R.layout.edit_boolean_habit, container, false); HabitsApplication app = (HabitsApplication) getContext().getApplicationContext(); @@ -201,7 +207,8 @@ public abstract class BaseDialog extends AppCompatDialogFragment int color = modifiedHabit.getColor(); ColorPickerDialog picker = colorPickerDialogFactory.create(color); - picker.setListener(c -> { + picker.setListener(c -> + { prefs.setDefaultHabitColor(c); modifiedHabit.setColor(c); helper.populateColor(c); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateBooleanHabitDialog.java similarity index 96% rename from app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java rename to app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateBooleanHabitDialog.java index 5f34e2e40..7d94f57c5 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateBooleanHabitDialog.java @@ -26,7 +26,7 @@ import org.isoron.uhabits.commands.*; import org.isoron.uhabits.models.*; @AutoFactory(allowSubclasses = true) -public class CreateHabitDialog extends BaseDialog +public class CreateBooleanHabitDialog extends BooleanHabitDialog { @Override protected int getTitle() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateNumericalHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateNumericalHabitDialog.java new file mode 100644 index 000000000..459a94aa1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateNumericalHabitDialog.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 Á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.edit; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; + +@AutoFactory(allowSubclasses = true) +public class CreateNumericalHabitDialog extends NumericalHabitDialog +{ + @Override + protected int getTitle() + { + return R.string.create_habit; + } + + @Override + protected void initializeHabits() + { + modifiedHabit = modelFactory.buildHabit(); + modifiedHabit.setFrequency(Frequency.DAILY); + modifiedHabit.setColor( + prefs.getDefaultHabitColor(modifiedHabit.getColor())); + modifiedHabit.setType(Habit.NUMBER_HABIT); + } + + @Override + protected void saveHabit() + { + Command command = appComponent + .getCreateHabitCommandFactory() + .create(habitList, modifiedHabit); + commandRunner.execute(command, null); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialog.java similarity index 96% rename from app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java rename to app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialog.java index e9c1aca78..ff5c33775 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialog.java @@ -22,7 +22,7 @@ package org.isoron.uhabits.activities.habits.edit; import org.isoron.uhabits.*; import org.isoron.uhabits.commands.*; -public class EditHabitDialog extends BaseDialog +public class EditBooleanHabitDialog extends BooleanHabitDialog { @Override protected int getTitle() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialogFactory.java similarity index 84% rename from app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java rename to app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialogFactory.java index 481658ebf..43cc27348 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditBooleanHabitDialogFactory.java @@ -26,19 +26,19 @@ import org.isoron.uhabits.models.*; import javax.inject.*; -public class EditHabitDialogFactory +public class EditBooleanHabitDialogFactory { @Inject - public EditHabitDialogFactory() + public EditBooleanHabitDialogFactory() { } - public EditHabitDialog create(@NonNull Habit habit) + public EditBooleanHabitDialog create(@NonNull Habit habit) { if (habit.getId() == null) throw new IllegalArgumentException("habit not saved"); - EditHabitDialog dialog = new EditHabitDialog(); + EditBooleanHabitDialog dialog = new EditBooleanHabitDialog(); Bundle args = new Bundle(); args.putLong("habitId", habit.getId()); dialog.setArguments(args); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/NumericalHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/NumericalHabitDialog.java new file mode 100644 index 000000000..3425f6663 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/NumericalHabitDialog.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2016 Á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.edit; + +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.text.format.*; +import android.view.*; + +import com.android.datetimepicker.time.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.R; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +import java.util.*; + +import butterknife.*; + +public abstract class NumericalHabitDialog extends AppCompatDialogFragment +{ + @Nullable + protected Habit originalHabit; + + @Nullable + protected Habit modifiedHabit; + + @Nullable + protected BaseDialogHelper helper; + + protected Preferences prefs; + + protected CommandRunner commandRunner; + + protected HabitList habitList; + + protected AppComponent appComponent; + + protected ModelFactory modelFactory; + + private ColorPickerDialogFactory colorPickerDialogFactory; + + @Override + public int getTheme() + { + return R.style.DialogWithTitle; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + + BaseActivity activity = (BaseActivity) getActivity(); + colorPickerDialogFactory = + activity.getComponent().getColorPickerDialogFactory(); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = + inflater.inflate(R.layout.edit_numerical_habit, container, false); + + HabitsApplication app = + (HabitsApplication) getContext().getApplicationContext(); + + appComponent = app.getComponent(); + prefs = appComponent.getPreferences(); + habitList = appComponent.getHabitList(); + commandRunner = appComponent.getCommandRunner(); + modelFactory = appComponent.getModelFactory(); + + ButterKnife.bind(this, view); + + helper = new BaseDialogHelper(this, view); + getDialog().setTitle(getTitle()); + initializeHabits(); + restoreSavedInstance(savedInstanceState); + helper.populateForm(modifiedHabit); + return view; + } + + @Override + @SuppressWarnings("ConstantConditions") + public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putInt("color", modifiedHabit.getColor()); + if (modifiedHabit.hasReminder()) + { + Reminder reminder = modifiedHabit.getReminder(); + outState.putInt("reminderMin", reminder.getMinute()); + outState.putInt("reminderHour", reminder.getHour()); + outState.putInt("reminderDays", reminder.getDays().toInteger()); + } + } + + protected abstract int getTitle(); + + protected abstract void initializeHabits(); + + protected void restoreSavedInstance(@Nullable Bundle bundle) + { + if (bundle == null) return; + modifiedHabit.setColor( + bundle.getInt("color", modifiedHabit.getColor())); + + modifiedHabit.setReminder(null); + + int hour = (bundle.getInt("reminderHour", -1)); + int minute = (bundle.getInt("reminderMin", -1)); + int days = (bundle.getInt("reminderDays", -1)); + + if (hour >= 0 && minute >= 0) + { + Reminder reminder = + new Reminder(hour, minute, new WeekdayList(days)); + modifiedHabit.setReminder(reminder); + } + } + + protected abstract void saveHabit(); + + @OnClick(R.id.buttonDiscard) + void onButtonDiscardClick() + { + dismiss(); + } + + @OnClick(R.id.tvReminderTime) + @SuppressWarnings("ConstantConditions") + void onDateSpinnerClick() + { + int defaultHour = 8; + int defaultMin = 0; + + if (modifiedHabit.hasReminder()) + { + Reminder reminder = modifiedHabit.getReminder(); + defaultHour = reminder.getHour(); + defaultMin = reminder.getMinute(); + } + + showTimePicker(defaultHour, defaultMin); + } + + @OnClick(R.id.buttonSave) + void onSaveButtonClick() + { + helper.parseFormIntoHabit(modifiedHabit); + if (!helper.validate(modifiedHabit)) return; + saveHabit(); + dismiss(); + } + + @OnClick(R.id.tvReminderDays) + @SuppressWarnings("ConstantConditions") + void onWeekdayClick() + { + if (!modifiedHabit.hasReminder()) return; + Reminder reminder = modifiedHabit.getReminder(); + + WeekdayPickerDialog dialog = new WeekdayPickerDialog(); + dialog.setListener(new OnWeekdaysPickedListener()); + dialog.setSelectedDays(reminder.getDays().toArray()); + dialog.show(getFragmentManager(), "weekdayPicker"); + } + + @OnClick(R.id.buttonPickColor) + void showColorPicker() + { + int color = modifiedHabit.getColor(); + ColorPickerDialog picker = colorPickerDialogFactory.create(color); + + picker.setListener(c -> + { + prefs.setDefaultHabitColor(c); + modifiedHabit.setColor(c); + helper.populateColor(c); + }); + + picker.show(getFragmentManager(), "picker"); + } + + private void showTimePicker(int defaultHour, int defaultMin) + { + boolean is24HourMode = DateFormat.is24HourFormat(getContext()); + TimePickerDialog timePicker = + TimePickerDialog.newInstance(new OnTimeSetListener(), defaultHour, + defaultMin, is24HourMode); + timePicker.show(getFragmentManager(), "timePicker"); + } + + private class OnTimeSetListener + implements TimePickerDialog.OnTimeSetListener + { + @Override + public void onTimeCleared(RadialPickerLayout view) + { + modifiedHabit.clearReminder(); + helper.populateReminderFields(modifiedHabit); + } + + @Override + public void onTimeSet(RadialPickerLayout view, int hour, int minute) + { + Reminder reminder = + new Reminder(hour, minute, WeekdayList.EVERY_DAY); + modifiedHabit.setReminder(reminder); + helper.populateReminderFields(modifiedHabit); + } + } + + private class OnWeekdaysPickedListener + implements WeekdayPickerDialog.OnWeekdaysPickedListener + { + @Override + public void onWeekdaysPicked(boolean[] selectedDays) + { + if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true); + + Reminder oldReminder = modifiedHabit.getReminder(); + modifiedHabit.setReminder( + new Reminder(oldReminder.getHour(), oldReminder.getMinute(), + new WeekdayList(selectedDays))); + helper.populateReminderFields(modifiedHabit); + } + + private boolean isSelectionEmpty(boolean[] selectedDays) + { + for (boolean d : selectedDays) if (d) return false; + return true; + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 486892cc7..afc3cc94b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -82,7 +82,8 @@ public class ListHabitsScreen extends BaseScreen private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; @NonNull - private final CreateHabitDialogFactory createHabitDialogFactory; + private final CreateBooleanHabitDialogFactory + createBooleanHabitDialogFactory; @NonNull private final FilePickerDialogFactory filePickerDialogFactory; @@ -91,11 +92,13 @@ public class ListHabitsScreen extends BaseScreen private final ColorPickerDialogFactory colorPickerFactory; @NonNull - private final EditHabitDialogFactory editHabitDialogFactory; + private final EditBooleanHabitDialogFactory editBooleanHabitDialogFactory; @NonNull private final ThemeSwitcher themeSwitcher; + private CreateNumericalHabitDialogFactory createNumericalHabitDialogFactory; + @Inject public ListHabitsScreen(@NonNull BaseActivity activity, @NonNull CommandRunner commandRunner, @@ -103,24 +106,21 @@ public class ListHabitsScreen extends BaseScreen @NonNull ListHabitsRootView rootView, @NonNull IntentFactory intentFactory, @NonNull ThemeSwitcher themeSwitcher, - @NonNull - ConfirmDeleteDialogFactory confirmDeleteDialogFactory, - @NonNull - CreateHabitDialogFactory createHabitDialogFactory, - @NonNull - FilePickerDialogFactory filePickerDialogFactory, - @NonNull - ColorPickerDialogFactory colorPickerFactory, - @NonNull - EditHabitDialogFactory editHabitDialogFactory) + @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, + @NonNull CreateBooleanHabitDialogFactory createBooleanHabitDialogFactory, + @NonNull FilePickerDialogFactory filePickerDialogFactory, + @NonNull ColorPickerDialogFactory colorPickerFactory, + @NonNull EditBooleanHabitDialogFactory editBooleanHabitDialogFactory, + @NonNull CreateNumericalHabitDialogFactory createNumericalHabitDialogFactory) { super(activity); setRootView(rootView); - this.editHabitDialogFactory = editHabitDialogFactory; this.colorPickerFactory = colorPickerFactory; this.commandRunner = commandRunner; this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; - this.createHabitDialogFactory = createHabitDialogFactory; + this.createNumericalHabitDialogFactory = createNumericalHabitDialogFactory; + this.createBooleanHabitDialogFactory = createBooleanHabitDialogFactory; + this.editBooleanHabitDialogFactory = editBooleanHabitDialogFactory; this.dirFinder = dirFinder; this.filePickerDialogFactory = filePickerDialogFactory; this.intentFactory = intentFactory; @@ -182,57 +182,41 @@ public class ListHabitsScreen extends BaseScreen public void showCreateHabitScreen() { - activity.showDialog(createHabitDialogFactory.create(), "editHabit"); + Dialog dialog = new AlertDialog.Builder(activity) + .setTitle("Type of habit") + .setItems(R.array.habitTypes, (d, which) -> { + if(which == 0) showCreateBooleanHabitScreen(); + else showCreateNumericalHabitScreen(); + }) + .create(); + + dialog.show(); } - public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback) + private void showCreateNumericalHabitScreen() { - activity.showDialog(confirmDeleteDialogFactory.create(callback)); + CreateNumericalHabitDialog dialog; + dialog = createNumericalHabitDialogFactory.create(); + activity.showDialog(dialog, "editHabit"); } - public void showEditHabitScreen(Habit habit) + public void showCreateBooleanHabitScreen() { - EditHabitDialog dialog = editHabitDialogFactory.create(habit); + CreateBooleanHabitDialog dialog; + dialog = createBooleanHabitDialogFactory.create(); activity.showDialog(dialog, "editHabit"); } - public void showNumberPicker(int initialValue, - @NonNull NumberPickerCallback callback) + public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback) { - LayoutInflater inflater = activity.getLayoutInflater(); - View view = inflater.inflate(R.layout.number_picker_dialog, null); - - final NumberPicker picker = - (NumberPicker) view.findViewById(R.id.picker); - - picker.setMinValue(0); - picker.setMaxValue(Integer.MAX_VALUE); - picker.setValue(initialValue); - picker.setWrapSelectorWheel(false); - - AlertDialog dialog = new AlertDialog.Builder(activity) - .setView(view) - .setTitle(R.string.change_value) - .setPositiveButton(android.R.string.ok, (d, which) -> { - picker.clearFocus(); - callback.onNumberPicked(picker.getValue()); - }).create(); - - InterfaceUtils.setupEditorAction(picker, (v, actionId, event) -> { - if (actionId == IME_ACTION_DONE) - dialog.getButton(BUTTON_POSITIVE).performClick(); - return false; - }); - - dialog.show(); + activity.showDialog(confirmDeleteDialogFactory.create(callback)); + } - Window window = dialog.getWindow(); - if (window != null) - { - int width = (int) dpToPixels(activity, 200); - int height = (int) dpToPixels(activity, 275); - window.setLayout(width, height); - } + public void showEditHabitScreen(Habit habit) + { + EditBooleanHabitDialog dialog; + dialog = editBooleanHabitDialogFactory.create(habit); + activity.showDialog(dialog, "editHabit"); } public void showFAQScreen() @@ -288,6 +272,48 @@ public class ListHabitsScreen extends BaseScreen activity.startActivity(intent); } + public void showNumberPicker(int initialValue, + @NonNull NumberPickerCallback callback) + { + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.number_picker_dialog, null); + + final NumberPicker picker = + (NumberPicker) view.findViewById(R.id.picker); + + picker.setMinValue(0); + picker.setMaxValue(Integer.MAX_VALUE); + picker.setValue(initialValue); + picker.setWrapSelectorWheel(false); + + AlertDialog dialog = new AlertDialog.Builder(activity) + .setView(view) + .setTitle(R.string.change_value) + .setPositiveButton(android.R.string.ok, (d, which) -> + { + picker.clearFocus(); + callback.onNumberPicked(picker.getValue()); + }) + .create(); + + InterfaceUtils.setupEditorAction(picker, (v, actionId, event) -> + { + if (actionId == IME_ACTION_DONE) + dialog.getButton(BUTTON_POSITIVE).performClick(); + return false; + }); + + dialog.show(); + + Window window = dialog.getWindow(); + if (window != null) + { + int width = (int) dpToPixels(activity, 200); + int height = (int) dpToPixels(activity, 275); + window.setLayout(width, height); + } + } + public void showSettingsScreen() { Intent intent = intentFactory.startSettingsActivity(activity); diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index febbfde84..30a3fa10b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -106,12 +106,6 @@ public class HabitCardView extends FrameLayout checkmarkPanel.setValues(values); numberPanel.setValues(values); numberPanel.setThreshold(10); - -// int[] magnitudes = new int[]{10, 100, 1000, 10000}; -// int threshold = magnitudes[new Random().nextInt(4)]; -// numberPanel.setThreshold(threshold); -// numberPanel.initEditMode(); - postInvalidate(); } @@ -239,9 +233,9 @@ public class HabitCardView extends FrameLayout checkmarkPanel.setColor(color); numberPanel.setColor(color); - boolean isNumberHabit = true; //(new Random().nextInt(3) == 0); - checkmarkPanel.setVisibility(isNumberHabit ? GONE : VISIBLE); - numberPanel.setVisibility(isNumberHabit ? VISIBLE : GONE); + boolean isNumerical = habit.isNumerical(); + checkmarkPanel.setVisibility(isNumerical ? GONE : VISIBLE); + numberPanel.setVisibility(isNumerical ? VISIBLE : GONE); postInvalidate(); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java index 1c238c02e..9a8d7d27d 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java @@ -38,17 +38,18 @@ public class ShowHabitScreen extends BaseScreen private ShowHabitController controller; @NonNull - private final EditHabitDialogFactory editHabitDialogFactory; + private final EditBooleanHabitDialogFactory editBooleanHabitDialogFactory; @Inject public ShowHabitScreen(@NonNull BaseActivity activity, @NonNull Habit habit, @NonNull ShowHabitRootView view, - @NonNull EditHabitDialogFactory editHabitDialogFactory) + @NonNull + EditBooleanHabitDialogFactory editBooleanHabitDialogFactory) { super(activity); setRootView(view); - this.editHabitDialogFactory = editHabitDialogFactory; + this.editBooleanHabitDialogFactory = editBooleanHabitDialogFactory; this.habit = habit; } @@ -71,7 +72,7 @@ public class ShowHabitScreen extends BaseScreen public void showEditHabitDialog() { - EditHabitDialog dialog = editHabitDialogFactory.create(habit); + EditBooleanHabitDialog dialog = editBooleanHabitDialogFactory.create(habit); activity.showDialog(dialog, "editHabit"); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 5d81ddd64..256e05add 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -36,6 +36,10 @@ public class Habit public static final String HABIT_URI_FORMAT = "content://org.isoron.uhabits/habit/%d"; + public static final int NUMBER_HABIT = 1; + + public static final int YES_NO_HABIT = 0; + @Nullable private Long id; @@ -69,6 +73,8 @@ public class Habit @Nullable private Reminder reminder; + private int type; + private ModelObservable observable = new ModelObservable(); /** @@ -83,6 +89,7 @@ public class Habit this.color = 5; this.archived = false; this.frequency = new Frequency(3, 7); + this.type = YES_NO_HABIT; checkmarks = factory.buildCheckmarkList(this); streaks = factory.buildStreakList(this); @@ -112,6 +119,7 @@ public class Habit this.archived = model.isArchived(); this.frequency = model.frequency; this.reminder = model.reminder; + this.type = model.type; observable.notifyListeners(); } @@ -232,6 +240,19 @@ public class Habit return streaks; } + public int getType() + { + return type; + } + + public void setType(int type) + { + if (type != YES_NO_HABIT && type != NUMBER_HABIT) + throw new IllegalArgumentException(); + + this.type = type; + } + /** * Returns the public URI that identifies this habit * @@ -253,6 +274,13 @@ public class Habit return reminder != null; } + public void invalidateNewerThan(long timestamp) + { + getScores().invalidateNewerThan(timestamp); + getCheckmarks().invalidateNewerThan(timestamp); + getStreaks().invalidateNewerThan(timestamp); + } + public boolean isArchived() { return archived; @@ -263,6 +291,11 @@ public class Habit this.archived = archived; } + public boolean isNumerical() + { + return type == NUMBER_HABIT; + } + @Override public String toString() { @@ -272,13 +305,7 @@ public class Habit .append("description", description) .append("color", color) .append("archived", archived) + .append("type", type) .toString(); } - - public void invalidateNewerThan(long timestamp) - { - getScores().invalidateNewerThan(timestamp); - getCheckmarks().invalidateNewerThan(timestamp); - getStreaks().invalidateNewerThan(timestamp); - } } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java index b4120a386..7b2c672b7 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java @@ -22,8 +22,6 @@ package org.isoron.uhabits.models.sqlite.records; import android.annotation.*; import android.database.*; import android.support.annotation.*; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import com.activeandroid.*; import com.activeandroid.annotation.*; @@ -44,7 +42,7 @@ public class HabitRecord extends Model implements SQLiteRecord public static String SELECT = "select id, color, description, freq_den, freq_num, " + "name, position, reminder_hour, reminder_min, " + - "highlight, archived, reminder_days from habits "; + "highlight, archived, reminder_days, type from habits "; @Column(name = "name") public String name; @@ -82,6 +80,9 @@ public class HabitRecord extends Model implements SQLiteRecord @Column(name = "archived") public Integer archived; + @Column(name = "type") + public Integer type; + public HabitRecord() { } @@ -146,6 +147,8 @@ public class HabitRecord extends Model implements SQLiteRecord this.highlight = 0; this.color = model.getColor(); this.archived = model.isArchived() ? 1 : 0; + this.type = model.getType(); + Frequency freq = model.getFrequency(); this.freqNum = freq.getNumerator(); this.freqDen = freq.getDenominator(); @@ -177,6 +180,7 @@ public class HabitRecord extends Model implements SQLiteRecord highlight = c.getInt(9); archived = c.getInt(10); reminderDays = c.getInt(11); + type = c.getInt(12); } public void copyTo(Habit habit) @@ -187,6 +191,7 @@ public class HabitRecord extends Model implements SQLiteRecord habit.setColor(this.color); habit.setArchived(this.archived != 0); habit.setId(this.getId()); + habit.setType(this.type); if (reminderHour != null && reminderMin != null) { diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_boolean_habit.xml similarity index 98% rename from app/src/main/res/layout/edit_habit.xml rename to app/src/main/res/layout/edit_boolean_habit.xml index 007fe9500..eda86d33d 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_boolean_habit.xml @@ -22,7 +22,7 @@ style="@style/dialogForm" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - tools:context=".activities.habits.edit.BaseDialog" + tools:context=".activities.habits.edit.BooleanHabitDialog" tools:ignore="MergeRootFrame"> + ~ + ~ 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 . + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +