+ *
+ * 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;
+
+import android.content.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.support.v7.app.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.habits.list.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.models.sqlite.*;
+
+import static android.R.anim.*;
+
+/**
+ * Base class for all activities in the application.
+ *
+ * This class delegates the responsibilities of an Android activity to other
+ * classes. For example, callbacks related to menus are forwarded to a {@link
+ * BaseMenu}, while callbacks related to activity results are forwarded to a
+ * {@link BaseScreen}.
+ *
+ * A BaseActivity also installs an {@link java.lang.Thread.UncaughtExceptionHandler}
+ * to the main thread that logs the exception to the disk before the application
+ * crashes.
+ */
+abstract public class BaseActivity extends AppCompatActivity
+ implements Thread.UncaughtExceptionHandler
+{
+ @Nullable
+ private BaseMenu baseMenu;
+
+ @Nullable
+ private Thread.UncaughtExceptionHandler androidExceptionHandler;
+
+ @Nullable
+ private BaseScreen screen;
+
+ private ActivityComponent component;
+
+ public ActivityComponent getComponent()
+ {
+ return component;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(@Nullable Menu menu)
+ {
+ if (menu == null) return true;
+ if (baseMenu == null) return true;
+ baseMenu.onCreate(getMenuInflater(), menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@Nullable MenuItem item)
+ {
+ if (item == null) return false;
+ if (baseMenu == null) return false;
+ return baseMenu.onItemSelected(item);
+ }
+
+ public void restartWithFade()
+ {
+ new Handler().postDelayed(() -> {
+ Intent intent = new Intent(this, ListHabitsActivity.class);
+ finish();
+ overridePendingTransition(fade_in, fade_out);
+ startActivity(intent);
+
+ }, 500); // HACK: Let the menu disappear first
+ }
+
+ public void setBaseMenu(@Nullable BaseMenu baseMenu)
+ {
+ this.baseMenu = baseMenu;
+ }
+
+ public void setScreen(@Nullable BaseScreen screen)
+ {
+ this.screen = screen;
+ }
+
+ public void showDialog(AppCompatDialogFragment dialog, String tag)
+ {
+ dialog.show(getSupportFragmentManager(), tag);
+ }
+
+ public void showDialog(AppCompatDialog dialog)
+ {
+ dialog.show();
+ }
+
+ @Override
+ public void uncaughtException(@Nullable Thread thread,
+ @Nullable Throwable ex)
+ {
+ if (ex == null) return;
+
+ try
+ {
+ ex.printStackTrace();
+ new BaseSystem(this).dumpBugReportToFile();
+ }
+ catch (Exception e)
+ {
+ // ignored
+ }
+
+ if (ex.getCause() instanceof InconsistentDatabaseException)
+ {
+ HabitsApplication app = (HabitsApplication) getApplication();
+ HabitList habits = app.getComponent().getHabitList();
+ habits.repair();
+ System.exit(0);
+ }
+
+ if (androidExceptionHandler != null)
+ androidExceptionHandler.uncaughtException(thread, ex);
+ else System.exit(1);
+ }
+
+ @Override
+ protected void onActivityResult(int request, int result, Intent data)
+ {
+ if (screen == null) super.onActivityResult(request, result, data);
+ else screen.onResult(request, result, data);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler(this);
+
+ HabitsApplication app = (HabitsApplication) getApplicationContext();
+
+ component = DaggerActivityComponent
+ .builder()
+ .activityModule(new ActivityModule(this))
+ .appComponent(app.getComponent())
+ .build();
+
+ component.getThemeSwitcher().apply();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseMenu.java b/app/src/main/java/org/isoron/uhabits/activities/BaseMenu.java
new file mode 100644
index 000000000..f9d7bf077
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/BaseMenu.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+import android.view.*;
+
+import javax.annotation.*;
+
+/**
+ * Base class for all the menus in the application.
+ *
+ * This class receives from BaseActivity all callbacks related to menus, such as
+ * menu creation and click events. It also handles some implementation details
+ * of creating menus in Android, such as inflating the resources.
+ */
+public abstract class BaseMenu
+{
+ @NonNull
+ private final BaseActivity activity;
+
+ public BaseMenu(@NonNull BaseActivity activity)
+ {
+ this.activity = activity;
+ }
+
+ /**
+ * Declare that the menu has changed, and should be recreated.
+ */
+ public void invalidate()
+ {
+ activity.invalidateOptionsMenu();
+ }
+
+ /**
+ * Called when the menu is first displayed.
+ *
+ * The given menu is already inflated and ready to receive items. The
+ * application should override this method and add items to the menu here.
+ *
+ * @param menu the menu that is being created.
+ */
+ public void onCreate(@NonNull Menu menu)
+ {
+ }
+
+ /**
+ * Called when the menu is first displayed.
+ *
+ * This method cannot be overridden. The application should override the
+ * methods onCreate(Menu) and getMenuResourceId instead.
+ *
+ * @param inflater a menu inflater, for creating the menu
+ * @param menu the menu that is being created.
+ */
+ public final void onCreate(@NonNull MenuInflater inflater,
+ @NonNull Menu menu)
+ {
+ menu.clear();
+ inflater.inflate(getMenuResourceId(), menu);
+ onCreate(menu);
+ }
+
+ /**
+ * Called whenever an item on the menu is selected.
+ *
+ * @param item the item that was selected.
+ * @return true if the event was consumed, or false otherwise
+ */
+ public boolean onItemSelected(@NonNull MenuItem item)
+ {
+ return false;
+ }
+
+ /**
+ * Returns the id of the resource that should be used to inflate this menu.
+ *
+ * @return id of the menu resource.
+ */
+ @Resource
+ protected abstract int getMenuResourceId();
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseRootView.java b/app/src/main/java/org/isoron/uhabits/activities/BaseRootView.java
new file mode 100644
index 000000000..af3867ead
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/BaseRootView.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.support.v4.content.res.*;
+import android.support.v7.widget.Toolbar;
+import android.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+
+import static android.os.Build.VERSION.*;
+import static android.os.Build.VERSION_CODES.*;
+
+/**
+ * Base class for all root views in the application.
+ *
+ * A root view is an Android view that is directly attached to an activity. This
+ * view usually includes a toolbar and a progress bar. This abstract class hides
+ * some of the complexity of setting these things up, for every version of
+ * Android.
+ */
+public abstract class BaseRootView extends FrameLayout
+{
+ private final Context context;
+
+ private final BaseActivity activity;
+
+ private final ThemeSwitcher themeSwitcher;
+
+ public BaseRootView(Context context)
+ {
+ super(context);
+ this.context = context;
+ activity = (BaseActivity) context;
+ themeSwitcher = activity.getComponent().getThemeSwitcher();
+ }
+
+ public boolean getDisplayHomeAsUp()
+ {
+ return false;
+ }
+
+ @NonNull
+ public abstract Toolbar getToolbar();
+
+ public int getToolbarColor()
+ {
+ if (SDK_INT < LOLLIPOP && !themeSwitcher.isNightMode())
+ {
+ return ResourcesCompat.getColor(context.getResources(),
+ R.color.grey_900, context.getTheme());
+ }
+
+ StyledResources res = new StyledResources(context);
+ return res.getColor(R.attr.colorPrimary);
+ }
+
+ protected void initToolbar()
+ {
+ if (SDK_INT >= LOLLIPOP)
+ {
+ getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2));
+
+ View view = findViewById(R.id.toolbarShadow);
+ if (view != null) view.setVisibility(GONE);
+
+ view = findViewById(R.id.headerShadow);
+ if(view != null) view.setVisibility(GONE);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java
new file mode 100644
index 000000000..1954c7c42
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java
@@ -0,0 +1,301 @@
+/*
+ * 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;
+
+import android.content.*;
+import android.graphics.*;
+import android.graphics.drawable.*;
+import android.net.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.support.design.widget.*;
+import android.support.v4.content.res.*;
+import android.support.v7.app.*;
+import android.support.v7.view.ActionMode;
+import android.support.v7.widget.Toolbar;
+import android.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+
+import java.io.*;
+
+import static android.os.Build.VERSION.*;
+import static android.os.Build.VERSION_CODES.*;
+
+/**
+ * Base class for all screens in the application.
+ *
+ * Screens are responsible for deciding what root views and what menus should be
+ * attached to the main window. They are also responsible for showing other
+ * screens and for receiving their results.
+ */
+public class BaseScreen
+{
+ protected BaseActivity activity;
+
+ @Nullable
+ private BaseRootView rootView;
+
+ @Nullable
+ private BaseSelectionMenu selectionMenu;
+
+ private Snackbar snackbar;
+
+ public BaseScreen(@NonNull BaseActivity activity)
+ {
+ this.activity = activity;
+ }
+
+ @Deprecated
+ public static void setupActionBarColor(@NonNull AppCompatActivity activity,
+ int color)
+ {
+
+ Toolbar toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
+ if (toolbar == null) return;
+
+ activity.setSupportActionBar(toolbar);
+
+ ActionBar actionBar = activity.getSupportActionBar();
+ if (actionBar == null) return;
+
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+
+ ColorDrawable drawable = new ColorDrawable(color);
+ actionBar.setBackgroundDrawable(drawable);
+
+ if (SDK_INT >= LOLLIPOP)
+ {
+ int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f);
+ activity.getWindow().setStatusBarColor(darkerColor);
+
+ toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2));
+
+ View view = activity.findViewById(R.id.toolbarShadow);
+ if (view != null) view.setVisibility(View.GONE);
+
+ view = activity.findViewById(R.id.headerShadow);
+ if (view != null) view.setVisibility(View.GONE);
+ }
+ }
+
+ @Deprecated
+ public static int getDefaultActionBarColor(Context context)
+ {
+ if (SDK_INT < LOLLIPOP)
+ {
+ return ResourcesCompat.getColor(context.getResources(),
+ R.color.grey_900, context.getTheme());
+ }
+ else
+ {
+ StyledResources res = new StyledResources(context);
+ return res.getColor(R.attr.colorPrimary);
+ }
+ }
+
+ /**
+ * Notifies the screen that its contents should be updated.
+ */
+ public void invalidate()
+ {
+ if (rootView == null) return;
+ rootView.invalidate();
+ }
+
+ public void invalidateToolbar()
+ {
+ if (rootView == null) return;
+
+ activity.runOnUiThread(() -> {
+ Toolbar toolbar = rootView.getToolbar();
+ activity.setSupportActionBar(toolbar);
+ ActionBar actionBar = activity.getSupportActionBar();
+ if (actionBar == null) return;
+
+ actionBar.setDisplayHomeAsUpEnabled(rootView.getDisplayHomeAsUp());
+
+ int color = rootView.getToolbarColor();
+ setActionBarColor(actionBar, color);
+ setStatusBarColor(color);
+ });
+ }
+
+ /**
+ * Called when another Activity has finished, and has returned some result.
+ *
+ * @param requestCode the request code originally supplied to {@link
+ * android.app.Activity#startActivityForResult(Intent,
+ * int, Bundle)}.
+ * @param resultCode the result code sent by the other activity.
+ * @param data an Intent containing extra data sent by the other
+ * activity.
+ * @see {@link android.app.Activity#onActivityResult(int, int, Intent)}
+ */
+ public void onResult(int requestCode, int resultCode, Intent data)
+ {
+ }
+
+ /**
+ * Sets the menu to be shown by this screen.
+ *
+ * This menu will be visible if when there is no active selection operation.
+ * If the provided menu is null, then no menu will be shown.
+ *
+ * @param menu the menu to be shown.
+ */
+ public void setMenu(@Nullable BaseMenu menu)
+ {
+ activity.setBaseMenu(menu);
+ }
+
+ /**
+ * Sets the root view for this screen.
+ *
+ * @param rootView the root view for this screen.
+ */
+ public void setRootView(@Nullable BaseRootView rootView)
+ {
+ this.rootView = rootView;
+ activity.setContentView(rootView);
+ if (rootView == null) return;
+
+ invalidateToolbar();
+ }
+
+ /**
+ * Sets the menu to be shown when a selection is active on the screen.
+ *
+ * @param menu the menu to be shown during a selection
+ */
+ public void setSelectionMenu(@Nullable BaseSelectionMenu menu)
+ {
+ this.selectionMenu = menu;
+ }
+
+ /**
+ * Shows a message on the screen.
+ *
+ * @param stringId the string resource id for this message.
+ */
+ public void showMessage(@StringRes Integer stringId)
+ {
+ if (stringId == null || rootView == null) return;
+ if (snackbar == null)
+ {
+ snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT);
+ int tvId = android.support.design.R.id.snackbar_text;
+ TextView tv = (TextView) snackbar.getView().findViewById(tvId);
+ tv.setTextColor(Color.WHITE);
+ }
+ else snackbar.setText(stringId);
+ snackbar.show();
+ }
+
+ public void showSendEmailScreen(@StringRes int toId,
+ @StringRes int subjectId,
+ String content)
+ {
+ String to = activity.getString(toId);
+ String subject = activity.getString(subjectId);
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("message/rfc822");
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ to });
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ intent.putExtra(Intent.EXTRA_TEXT, content);
+ activity.startActivity(intent);
+ }
+
+ public void showSendFileScreen(@NonNull String archiveFilename)
+ {
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("application/zip");
+ intent.putExtra(Intent.EXTRA_STREAM,
+ Uri.fromFile(new File(archiveFilename)));
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Instructs the screen to start a selection.
+ *
+ * If a selection menu was provided, this menu will be shown instead of the
+ * regular one.
+ */
+ public void startSelection()
+ {
+ activity.startSupportActionMode(new ActionModeWrapper());
+ }
+
+ private void setActionBarColor(@NonNull ActionBar actionBar, int color)
+ {
+ ColorDrawable drawable = new ColorDrawable(color);
+ actionBar.setBackgroundDrawable(drawable);
+ }
+
+ private void setStatusBarColor(int baseColor)
+ {
+ if (SDK_INT < LOLLIPOP) return;
+
+ int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f);
+ activity.getWindow().setStatusBarColor(darkerColor);
+ }
+
+ private class ActionModeWrapper implements ActionMode.Callback
+ {
+ @Override
+ public boolean onActionItemClicked(@Nullable ActionMode mode,
+ @Nullable MenuItem item)
+ {
+ if (item == null || selectionMenu == null) return false;
+ return selectionMenu.onItemClicked(item);
+ }
+
+ @Override
+ public boolean onCreateActionMode(@Nullable ActionMode mode,
+ @Nullable Menu menu)
+ {
+ if (selectionMenu == null) return false;
+ if (mode == null || menu == null) return false;
+ selectionMenu.onCreate(activity.getMenuInflater(), mode, menu);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(@Nullable ActionMode mode)
+ {
+ if (selectionMenu == null) return;
+ selectionMenu.onFinish();
+ }
+
+ @Override
+ public boolean onPrepareActionMode(@Nullable ActionMode mode,
+ @Nullable Menu menu)
+ {
+ if (selectionMenu == null || menu == null) return false;
+ return selectionMenu.onPrepare(menu);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/activities/BaseSelectionMenu.java
new file mode 100644
index 000000000..a5c7f5cca
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/BaseSelectionMenu.java
@@ -0,0 +1,129 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+import android.support.v7.view.ActionMode;
+import android.view.*;
+
+/**
+ * Base class for all the selection menus in the application.
+ *
+ * A selection menu is a menu that appears when the screen starts a selection
+ * operation. It contains actions that modify the selected items, such as delete
+ * or archive. Since it replaces the toolbar, it also has a title.
+ *
+ * This class hides many implementation details of creating such menus in
+ * Android. The interface is supposed to look very similar to {@link BaseMenu},
+ * with a few additional methods, such as finishing the selection operation.
+ * Internally, it uses an {@link ActionMode}.
+ */
+public abstract class BaseSelectionMenu
+{
+ @Nullable
+ private ActionMode actionMode;
+
+ /**
+ * Finishes the selection operation.
+ */
+ public void finish()
+ {
+ if (actionMode != null) actionMode.finish();
+ }
+
+ /**
+ * Declare that the menu has changed, and should be recreated.
+ */
+ public void invalidate()
+ {
+ if (actionMode != null) actionMode.invalidate();
+ }
+
+ /**
+ * Called when the menu is first displayed.
+ *
+ * This method cannot be overridden. The application should override the
+ * methods onCreate(Menu) and getMenuResourceId instead.
+ *
+ * @param inflater a menu inflater, for creating the menu
+ * @param mode the action mode associated with this menu.
+ * @param menu the menu that is being created.
+ */
+ public final void onCreate(@NonNull MenuInflater inflater,
+ @NonNull ActionMode mode,
+ @NonNull Menu menu)
+ {
+ this.actionMode = mode;
+ inflater.inflate(getResourceId(), menu);
+ onCreate(menu);
+ }
+
+ /**
+ * Called when the selection operation is about to finish.
+ */
+ public void onFinish()
+ {
+
+ }
+
+ /**
+ * Called whenever an item on the menu is selected.
+ *
+ * @param item the item that was selected.
+ * @return true if the event was consumed, or false otherwise
+ */
+ public boolean onItemClicked(@NonNull MenuItem item)
+ {
+ return false;
+ }
+
+
+ /**
+ * Called whenever the menu is invalidated.
+ *
+ * @param menu the menu to be refreshed
+ * @return true if the menu has changes, false otherwise
+ */
+ public boolean onPrepare(@NonNull Menu menu)
+ {
+ return false;
+ }
+
+ /**
+ * Sets the title of the selection menu.
+ *
+ * @param title the new title.
+ */
+ public void setTitle(String title)
+ {
+ if (actionMode != null) actionMode.setTitle(title);
+ }
+
+ protected abstract int getResourceId();
+
+ /**
+ * Called when the menu is first created.
+ *
+ * @param menu the menu being created
+ */
+ protected void onCreate(@NonNull Menu menu)
+ {
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseSystem.java b/app/src/main/java/org/isoron/uhabits/activities/BaseSystem.java
new file mode 100644
index 000000000..044fa9600
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/BaseSystem.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import android.content.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+
+import java.io.*;
+import java.lang.Process;
+import java.util.*;
+
+import javax.inject.*;
+
+/**
+ * Base class for all systems class in the application.
+ *
+ * Classes derived from BaseSystem are responsible for handling events and
+ * sending requests to the Android operating system. Examples include capturing
+ * a bug report, obtaining device information, or requesting runtime
+ * permissions.
+ */
+@ActivityScope
+public class BaseSystem
+{
+ private Context context;
+
+ @Inject
+ public BaseSystem(@ActivityContext Context context)
+ {
+ this.context = context;
+ }
+
+ /**
+ * Captures a bug report and saves it to a file in the SD card.
+ *
+ * The contents of the file are generated by the method {@link
+ * #getBugReport()}. The file is saved in the apps's external private
+ * storage.
+ *
+ * @return the generated file.
+ * @throws IOException when I/O errors occur.
+ */
+ @NonNull
+ public File dumpBugReportToFile() throws IOException
+ {
+ String date =
+ DateFormats.getBackupDateFormat().format(DateUtils.getLocalTime());
+
+ if (context == null) throw new RuntimeException(
+ "application context should not be null");
+ File dir = FileUtils.getFilesDir("Logs");
+ if (dir == null) throw new IOException("log dir should not be null");
+
+ File logFile =
+ new File(String.format("%s/Log %s.txt", dir.getPath(), date));
+ FileWriter output = new FileWriter(logFile);
+ output.write(getBugReport());
+ output.close();
+
+ return logFile;
+ }
+
+ /**
+ * Captures and returns a bug report.
+ *
+ * The bug report contains some device information and the logcat.
+ *
+ * @return a String containing the bug report.
+ * @throws IOException when any I/O error occur.
+ */
+ @NonNull
+ public String getBugReport() throws IOException
+ {
+ String logcat = getLogcat();
+ String deviceInfo = getDeviceInfo();
+
+ String log = "---------- BUG REPORT BEGINS ----------\n";
+ log += deviceInfo + "\n" + logcat;
+ log += "---------- BUG REPORT ENDS ------------\n";
+
+ return log;
+ }
+
+ public String getLogcat() throws IOException
+ {
+ int maxLineCount = 250;
+ StringBuilder builder = new StringBuilder();
+
+ String[] command = new String[]{ "logcat", "-d" };
+ Process process = Runtime.getRuntime().exec(command);
+
+ InputStreamReader in = new InputStreamReader(process.getInputStream());
+ BufferedReader bufferedReader = new BufferedReader(in);
+
+ LinkedList log = new LinkedList<>();
+
+ String line;
+ while ((line = bufferedReader.readLine()) != null)
+ {
+ log.addLast(line);
+ if (log.size() > maxLineCount) log.removeFirst();
+ }
+
+ for (String l : log)
+ {
+ builder.append(l);
+ builder.append('\n');
+ }
+
+ return builder.toString();
+ }
+
+ private String getDeviceInfo()
+ {
+ if (context == null) return "null context\n";
+
+ WindowManager wm =
+ (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+ return
+ String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
+ String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
+ String.format("OS Version: %s (%s)\n",
+ System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
+ String.format("OS API Level: %s\n", Build.VERSION.SDK) +
+ String.format("Device: %s\n", Build.DEVICE) +
+ String.format("Model (Product): %s (%s)\n", Build.MODEL,
+ Build.PRODUCT) +
+ String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
+ String.format("Other tags: %s\n", Build.TAGS) +
+ String.format("Screen Width: %s\n",
+ wm.getDefaultDisplay().getWidth()) +
+ String.format("Screen Height: %s\n",
+ wm.getDefaultDisplay().getHeight()) +
+ String.format("External storage state: %s\n\n",
+ Environment.getExternalStorageState());
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.java b/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.java
new file mode 100644
index 000000000..d560800ec
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.preferences.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ThemeSwitcher
+{
+ public static final int THEME_DARK = 1;
+
+ public static final int THEME_LIGHT = 0;
+
+ @NonNull
+ private final BaseActivity activity;
+
+ private Preferences preferences;
+
+ @Inject
+ public ThemeSwitcher(@NonNull BaseActivity activity,
+ @NonNull Preferences preferences)
+ {
+ this.activity = activity;
+ this.preferences = preferences;
+ }
+
+ public void apply()
+ {
+ switch (getTheme())
+ {
+ case THEME_DARK:
+ applyDarkTheme();
+ break;
+
+ case THEME_LIGHT:
+ default:
+ applyLightTheme();
+ break;
+ }
+ }
+
+ public boolean isNightMode()
+ {
+ return getTheme() == THEME_DARK;
+ }
+
+ public void refreshTheme()
+ {
+
+ }
+
+ public void toggleNightMode()
+ {
+ if (isNightMode()) setTheme(THEME_LIGHT);
+ else setTheme(THEME_DARK);
+ }
+
+ private void applyDarkTheme()
+ {
+ if (preferences.isPureBlackEnabled())
+ activity.setTheme(R.style.AppBaseThemeDark_PureBlack);
+ else activity.setTheme(R.style.AppBaseThemeDark);
+ }
+
+ private void applyLightTheme()
+ {
+ activity.setTheme(R.style.AppBaseTheme);
+ }
+
+ private int getTheme()
+ {
+ return preferences.getTheme();
+ }
+
+ public void setTheme(int theme)
+ {
+ preferences.setTheme(theme);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.java b/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.java
new file mode 100644
index 000000000..b887766a5
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.about;
+
+import android.os.*;
+
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.intents.*;
+
+/**
+ * Activity that allows the user to see information about the app itself.
+ * Display current version, link to Google Play and list of contributors.
+ */
+public class AboutActivity extends BaseActivity
+{
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ AboutRootView rootView = new AboutRootView(this, new IntentFactory());
+ BaseScreen screen = new BaseScreen(this);
+ screen.setRootView(rootView);
+ setScreen(screen);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.java b/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.java
new file mode 100644
index 000000000..a8ff3abe7
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.java
@@ -0,0 +1,117 @@
+/*
+ * 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.about;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.support.v7.widget.Toolbar;
+import android.widget.*;
+
+import org.isoron.uhabits.BuildConfig;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.intents.*;
+import org.isoron.uhabits.utils.*;
+
+import butterknife.*;
+
+public class AboutRootView extends BaseRootView
+{
+ @BindView(R.id.tvVersion)
+ TextView tvVersion;
+
+ @BindView(R.id.tvRate)
+ TextView tvRate;
+
+ @BindView(R.id.tvFeedback)
+ TextView tvFeedback;
+
+ @BindView(R.id.tvSource)
+ TextView tvSource;
+
+ @BindView(R.id.toolbar)
+ Toolbar toolbar;
+
+ private final IntentFactory intents;
+
+ public AboutRootView(Context context, IntentFactory intents)
+ {
+ super(context);
+ this.intents = intents;
+
+ addView(inflate(getContext(), R.layout.about, null));
+ ButterKnife.bind(this);
+
+ tvVersion.setText(
+ String.format(getResources().getString(R.string.version_n),
+ BuildConfig.VERSION_NAME));
+ }
+
+ @Override
+ public boolean getDisplayHomeAsUp()
+ {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public Toolbar getToolbar()
+ {
+ return toolbar;
+ }
+
+ @Override
+ public int getToolbarColor()
+ {
+ StyledResources res = new StyledResources(getContext());
+ if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
+ return super.getToolbarColor();
+
+ return res.getColor(R.attr.aboutScreenColor);
+ }
+
+ @OnClick(R.id.tvFeedback)
+ public void onClickFeedback()
+ {
+ Intent intent = intents.sendFeedback(getContext());
+ getContext().startActivity(intent);
+ }
+
+ @OnClick(R.id.tvRate)
+ public void onClickRate()
+ {
+ Intent intent = intents.rateApp(getContext());
+ getContext().startActivity(intent);
+ }
+
+ @OnClick(R.id.tvSource)
+ public void onClickSource()
+ {
+ Intent intent = intents.viewSourceCode(getContext());
+ getContext().startActivity(intent);
+ }
+
+ @Override
+ protected void initToolbar()
+ {
+ super.initToolbar();
+ toolbar.setTitle(getResources().getString(R.string.about));
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/about/package-info.java
new file mode 100644
index 000000000..e519806ab
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/about/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides activity that shows information about the app.
+ */
+package org.isoron.uhabits.activities.about;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java
new file mode 100644
index 000000000..c4ab3022c
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java
@@ -0,0 +1,41 @@
+/*
+ * 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.common.dialogs;
+
+import org.isoron.uhabits.utils.*;
+
+/**
+ * Dialog that allows the user to choose a color.
+ */
+public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog
+{
+ public void setListener(OnColorSelectedListener listener)
+ {
+ super.setOnColorSelectedListener(c -> {
+ c = ColorUtils.colorToPaletteIndex(getContext(), c);
+ listener.onColorSelected(c);
+ });
+ }
+
+ public interface OnColorSelectedListener
+ {
+ void onColorSelected(int color);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java
new file mode 100644
index 000000000..e13368916
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.dialogs;
+
+import android.content.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.utils.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ColorPickerDialogFactory
+{
+ private final Context context;
+
+ @Inject
+ public ColorPickerDialogFactory(@ActivityContext Context context)
+ {
+ this.context = context;
+ }
+
+ public ColorPickerDialog create(int paletteColor)
+ {
+ ColorPickerDialog dialog = new ColorPickerDialog();
+ StyledResources res = new StyledResources(context);
+ int color = ColorUtils.getColor(context, paletteColor);
+
+ dialog.initialize(R.string.color_picker_default_title, res.getPalette(),
+ color, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL);
+
+ return dialog;
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java
new file mode 100644
index 000000000..b86ed2f25
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java
@@ -0,0 +1,63 @@
+/*
+ * 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.common.dialogs;
+
+import android.content.*;
+import android.support.v7.app.*;
+
+import com.google.auto.factory.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+
+import butterknife.*;
+
+/**
+ * Dialog that asks the user confirmation before executing a delete operation.
+ */
+@AutoFactory(allowSubclasses = true)
+public class ConfirmDeleteDialog extends AlertDialog
+{
+ @BindString(R.string.delete_habits_message)
+ protected String question;
+
+ @BindString(android.R.string.yes)
+ protected String yes;
+
+ @BindString(android.R.string.no)
+ protected String no;
+
+ protected ConfirmDeleteDialog(@Provided @ActivityContext Context context,
+ Callback callback)
+ {
+ super(context);
+ ButterKnife.bind(this);
+
+ setTitle(R.string.delete_habits);
+ setMessage(question);
+ setButton(BUTTON_POSITIVE, yes, (dialog, which) -> callback.run());
+ setButton(BUTTON_NEGATIVE, no, (dialog, which) -> {});
+ }
+
+ public interface Callback
+ {
+ void run();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java
similarity index 72%
rename from app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java
index 94c574fbd..a2cca7bbc 100644
--- a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java
@@ -17,63 +17,73 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.dialogs;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.support.annotation.NonNull;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager.LayoutParams;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.util.Arrays;
+package org.isoron.uhabits.activities.common.dialogs;
+import android.content.*;
+import android.support.annotation.*;
+import android.support.v7.app.*;
+import android.view.*;
+import android.view.WindowManager.*;
+import android.widget.*;
+
+import com.google.auto.factory.*;
+
+import org.isoron.uhabits.activities.*;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Dialog that allows the user to pick a file.
+ */
+@AutoFactory(allowSubclasses = true)
public class FilePickerDialog implements AdapterView.OnItemClickListener
{
private static final String PARENT_DIR = "..";
- private final Activity activity;
+ private final Context context;
+
private ListView list;
- private Dialog dialog;
- private File currentPath;
- public interface OnFileSelectedListener
- {
- void onFileSelected(File file);
- }
+ private AppCompatDialog dialog;
+
+ private File currentPath;
private OnFileSelectedListener listener;
- public FilePickerDialog(Activity activity, File initialDirectory)
+ public FilePickerDialog(@Provided @ActivityContext Context context,
+ File initialDirectory)
{
- this.activity = activity;
+ this.context = context;
- list = new ListView(activity);
+ list = new ListView(context);
list.setOnItemClickListener(this);
- dialog = new Dialog(activity);
+ dialog = new AppCompatDialog(context);
dialog.setContentView(list);
- dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ dialog
+ .getWindow()
+ .setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
navigateTo(initialDirectory);
}
+ public AppCompatDialog getDialog()
+ {
+ return dialog;
+ }
+
@Override
- public void onItemClick(AdapterView> parent, View view, int which, long id)
+ public void onItemClick(AdapterView> parent,
+ View view,
+ int which,
+ long id)
{
String filename = (String) list.getItemAtPosition(which);
File file;
- if (filename.equals(PARENT_DIR))
- file = currentPath.getParentFile();
- else
- file = new File(currentPath, filename);
+ if (filename.equals(PARENT_DIR)) file = currentPath.getParentFile();
+ else file = new File(currentPath, filename);
if (file.isDirectory())
{
@@ -86,27 +96,14 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
}
}
- public void show()
- {
- dialog.show();
- }
-
public void setListener(OnFileSelectedListener listener)
{
this.listener = listener;
}
- private void navigateTo(File path)
+ public void show()
{
- if (!path.exists()) return;
-
- File[] dirs = path.listFiles(new ReadableDirFilter());
- File[] files = path.listFiles(new RegularReadableFileFilter());
- if(dirs == null || files == null) return;
-
- this.currentPath = path;
- dialog.setTitle(currentPath.getPath());
- list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files)));
+ dialog.show();
}
@NonNull
@@ -138,11 +135,39 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
return fileList;
}
+ private void navigateTo(File path)
+ {
+ if (!path.exists()) return;
+
+ File[] dirs = path.listFiles(new ReadableDirFilter());
+ File[] files = path.listFiles(new RegularReadableFileFilter());
+ if (dirs == null || files == null) return;
+
+ this.currentPath = path;
+ dialog.setTitle(currentPath.getPath());
+ list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files)));
+ }
+
+ public interface OnFileSelectedListener
+ {
+ void onFileSelected(File file);
+ }
+
+ private static class ReadableDirFilter implements FileFilter
+ {
+ @Override
+ public boolean accept(File file)
+ {
+ return (file.isDirectory() && file.canRead());
+ }
+ }
+
private class FilePickerAdapter extends ArrayAdapter
{
public FilePickerAdapter(@NonNull String[] fileList)
{
- super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList);
+ super(FilePickerDialog.this.context,
+ android.R.layout.simple_list_item_1, fileList);
}
@Override
@@ -155,15 +180,6 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
}
}
- private static class ReadableDirFilter implements FileFilter
- {
- @Override
- public boolean accept(File file)
- {
- return (file.isDirectory() && file.canRead());
- }
- }
-
private class RegularReadableFileFilter implements FileFilter
{
@Override
diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java
new file mode 100644
index 000000000..7aac7e87a
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java
@@ -0,0 +1,174 @@
+/*
+ * 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.common.dialogs;
+
+import android.app.*;
+import android.content.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.*;
+import android.util.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+public class HistoryEditorDialog extends AppCompatDialogFragment
+ implements DialogInterface.OnClickListener, ModelObservable.Listener
+{
+ @Nullable
+ private Habit habit;
+
+ @Nullable
+ HistoryChart historyChart;
+
+ @NonNull
+ private Controller controller;
+
+ private HabitList habitList;
+
+ private TaskRunner taskRunner;
+
+ public HistoryEditorDialog()
+ {
+ this.controller = new Controller() {};
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ dismiss();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
+ {
+ Context context = getActivity();
+
+ HabitsApplication app =
+ (HabitsApplication) getActivity().getApplicationContext();
+ habitList = app.getComponent().getHabitList();
+ taskRunner = app.getComponent().getTaskRunner();
+
+ historyChart = new HistoryChart(context);
+ historyChart.setController(controller);
+
+ if (savedInstanceState != null)
+ {
+ long id = savedInstanceState.getLong("habit", -1);
+ if (id > 0) this.habit = habitList.getById(id);
+ }
+
+ int padding =
+ (int) getResources().getDimension(R.dimen.history_editor_padding);
+
+ historyChart.setPadding(padding, 0, padding, 0);
+ historyChart.setIsEditable(true);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder
+ .setTitle(R.string.history)
+ .setView(historyChart)
+ .setPositiveButton(android.R.string.ok, this);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onModelChange()
+ {
+ refreshData();
+ }
+
+ @Override
+ public void onPause()
+ {
+ habit.getCheckmarks().observable.removeListener(this);
+ super.onPause();
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ int maxHeight = getResources().getDimensionPixelSize(
+ R.dimen.history_editor_max_height);
+ int width = metrics.widthPixels;
+ int height = Math.min(metrics.heightPixels, maxHeight);
+
+ getDialog().getWindow().setLayout(width, height);
+
+ refreshData();
+ habit.getCheckmarks().observable.addListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState)
+ {
+ outState.putLong("habit", habit.getId());
+ }
+
+ public void setController(@NonNull Controller controller)
+ {
+ this.controller = controller;
+ if (historyChart != null) historyChart.setController(controller);
+ }
+
+ public void setHabit(@Nullable Habit habit)
+ {
+ this.habit = habit;
+ }
+
+ private void refreshData()
+ {
+ if (habit == null) return;
+ taskRunner.execute(new RefreshTask());
+ }
+
+ public interface Controller extends HistoryChart.Controller {}
+
+ private class RefreshTask implements Task
+ {
+ public int[] checkmarks;
+
+ @Override
+ public void doInBackground()
+ {
+ checkmarks = habit.getCheckmarks().getAllValues();
+ }
+
+ @Override
+ public void onPostExecute()
+ {
+ if (getContext() == null || habit == null || historyChart == null)
+ return;
+
+ int color = ColorUtils.getColor(getContext(), habit.getColor());
+ historyChart.setColor(color);
+ historyChart.setCheckmarks(checkmarks);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java
similarity index 64%
rename from app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java
index 5297b80eb..792e403f7 100644
--- a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java
@@ -17,67 +17,69 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.dialogs;
+package org.isoron.uhabits.activities.common.dialogs;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
+import android.app.*;
+import android.content.*;
+import android.os.*;
import android.support.v7.app.AlertDialog;
-import android.support.v7.app.AppCompatDialogFragment;
+import android.support.v7.app.*;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.DateHelper;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
-public class WeekdayPickerDialog extends AppCompatDialogFragment
- implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener
+/**
+ * Dialog that allows the user to pick one or more days of the week.
+ */
+public class WeekdayPickerDialog extends AppCompatDialogFragment implements
+ DialogInterface.OnMultiChoiceClickListener,
+ DialogInterface.OnClickListener
{
- public interface OnWeekdaysPickedListener
- {
- void onWeekdaysPicked(boolean[] selectedDays);
- }
-
private boolean[] selectedDays;
+
private OnWeekdaysPickedListener listener;
- public void setListener(OnWeekdaysPickedListener listener)
+ @Override
+ public void onClick(DialogInterface dialog, int which, boolean isChecked)
{
- this.listener = listener;
+ selectedDays[which] = isChecked;
}
- public void setSelectedDays(boolean[] selectedDays)
+ @Override
+ public void onClick(DialogInterface dialog, int which)
{
- this.selectedDays = selectedDays;
+ if (listener != null) listener.onWeekdaysPicked(selectedDays);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setTitle(R.string.select_weekdays)
- .setMultiChoiceItems(DateHelper.getLongDayNames(), selectedDays, this)
- .setPositiveButton(android.R.string.yes, this)
- .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
- {
- @Override
- public void onClick(DialogInterface dialog, int which)
- {
- dismiss();
- }
- });
+ builder
+ .setTitle(R.string.select_weekdays)
+ .setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays,
+ this)
+ .setPositiveButton(android.R.string.yes, this)
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ dismiss();
+ });
return builder.create();
}
- @Override
- public void onClick(DialogInterface dialog, int which, boolean isChecked)
+ public void setListener(OnWeekdaysPickedListener listener)
{
- selectedDays[which] = isChecked;
+ this.listener = listener;
}
- @Override
- public void onClick(DialogInterface dialog, int which)
+ public void setSelectedDays(boolean[] selectedDays)
{
- if(listener != null) listener.onWeekdaysPicked(selectedDays);
+ this.selectedDays = selectedDays;
+ }
+
+ public interface OnWeekdaysPickedListener
+ {
+ void onWeekdaysPicked(boolean[] selectedDays);
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java
similarity index 63%
rename from app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java
index 7890757dd..fa1d38214 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java
@@ -17,103 +17,104 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.helpers.DateHelper;
-import org.isoron.uhabits.helpers.UIHelper;
-import org.isoron.uhabits.models.Habit;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.Random;
-
-public class HabitFrequencyView extends ScrollableDataView implements HabitDataView
-{
+package org.isoron.uhabits.activities.common.views;
+
+import android.content.*;
+import android.graphics.*;
+import android.support.annotation.*;
+import android.util.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+import java.text.*;
+import java.util.*;
+
+public class FrequencyChart extends ScrollableChart
+{
private Paint pGrid;
+
private float em;
- private Habit habit;
+
private SimpleDateFormat dfMonth;
+
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
+
private RectF rect, prevRect;
+
private int baseSize;
+
private int paddingTop;
private float columnWidth;
+
private int columnHeight;
+
private int nColumns;
private int textColor;
+
private int gridColor;
+
private int[] colors;
+
private int primaryColor;
+
private boolean isBackgroundTransparent;
+ @NonNull
private HashMap frequency;
+ private int maxFreq;
- public HabitFrequencyView(Context context)
+ public FrequencyChart(Context context)
{
super(context);
init();
}
- public HabitFrequencyView(Context context, AttributeSet attrs)
+ public FrequencyChart(Context context, AttributeSet attrs)
{
super(context, attrs);
- this.primaryColor = ColorHelper.getColor(getContext(), 7);
this.frequency = new HashMap<>();
init();
}
- public void setHabit(Habit habit)
+ public void setColor(int color)
{
- this.habit = habit;
- createColors();
+ this.primaryColor = color;
+ initColors();
+ postInvalidate();
}
- private void init()
+ public void setFrequency(HashMap frequency)
{
- createPaints();
- createColors();
-
- dfMonth = DateHelper.getDateFormat("MMM");
- dfYear = DateHelper.getDateFormat("yyyy");
-
- rect = new RectF();
- prevRect = new RectF();
+ this.frequency = frequency;
+ maxFreq = getMaxFreq(frequency);
+ postInvalidate();
}
- private void createColors()
+ private int getMaxFreq(HashMap frequency)
{
- if(habit != null)
+ int maxValue = 1;
+ for (Integer[] values : frequency.values())
{
- this.primaryColor = ColorHelper.getColor(getContext(), habit.color);
+ for (Integer value : values)
+ {
+ maxValue = Math.max(value, maxValue);
+ }
}
+ return maxValue;
+ }
- textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
- gridColor = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor);
-
- colors = new int[4];
- colors[0] = gridColor;
- colors[3] = primaryColor;
- colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
- colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
+ public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ {
+ this.isBackgroundTransparent = isBackgroundTransparent;
+ initColors();
}
- protected void createPaints()
+ protected void initPaints()
{
pText = new Paint();
pText.setAntiAlias(true);
@@ -126,79 +127,6 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
pGrid.setAntiAlias(true);
}
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- setMeasuredDimension(width, height);
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
- {
- if(height < 9) height = 200;
-
- baseSize = height / 8;
- setScrollerBucketSize(baseSize);
-
- pText.setTextSize(baseSize * 0.4f);
- pGraph.setTextSize(baseSize * 0.4f);
- pGraph.setStrokeWidth(baseSize * 0.1f);
- pGrid.setStrokeWidth(baseSize * 0.05f);
- em = pText.getFontSpacing();
-
- columnWidth = baseSize;
- columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
-
- columnHeight = 8 * baseSize;
- nColumns = (int) (width / columnWidth);
- paddingTop = 0;
- }
-
- private float getMaxMonthWidth()
- {
- float maxMonthWidth = 0;
- GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
-
- for(int i = 0; i < 12; i++)
- {
- day.set(Calendar.MONTH, i);
- float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
- maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
- }
-
- return maxMonthWidth;
- }
-
- public void refreshData()
- {
- if(isInEditMode())
- generateRandomData();
- else if(habit != null)
- frequency = habit.repetitions.getWeekdayFrequency();
-
- postInvalidate();
- }
-
- private void generateRandomData()
- {
- GregorianCalendar date = DateHelper.getStartOfTodayCalendar();
- date.set(Calendar.DAY_OF_MONTH, 1);
- Random rand = new Random();
- frequency.clear();
-
- for(int i = 0; i < 40; i++)
- {
- Integer values[] = new Integer[7];
- for(int j = 0; j < 7; j++)
- values[j] = rand.nextInt(5);
-
- frequency.put(date.getTimeInMillis(), values);
- date.add(Calendar.MONTH, -1);
- }
- }
-
@Override
protected void onDraw(Canvas canvas)
{
@@ -214,12 +142,12 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
pGraph.setColor(primaryColor);
prevRect.setEmpty();
- GregorianCalendar currentDate = DateHelper.getStartOfTodayCalendar();
+ GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendar();
currentDate.set(Calendar.DAY_OF_MONTH, 1);
currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset());
- for(int i = 0; i < nColumns - 1; i++)
+ for (int i = 0; i < nColumns - 1; i++)
{
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(i * columnWidth, 0);
@@ -229,21 +157,53 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
}
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onSizeChanged(int width,
+ int height,
+ int oldWidth,
+ int oldHeight)
+ {
+ if (height < 9) height = 200;
+
+ baseSize = height / 8;
+ setScrollerBucketSize(baseSize);
+
+ pText.setTextSize(baseSize * 0.4f);
+ pGraph.setTextSize(baseSize * 0.4f);
+ pGraph.setStrokeWidth(baseSize * 0.1f);
+ pGrid.setStrokeWidth(baseSize * 0.05f);
+ em = pText.getFontSpacing();
+
+ columnWidth = baseSize;
+ columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
+
+ columnHeight = 8 * baseSize;
+ nColumns = (int) (width / columnWidth);
+ paddingTop = 0;
+ }
+
private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
{
Integer values[] = frequency.get(date.getTimeInMillis());
float rowHeight = rect.height() / 8.0f;
prevRect.set(rect);
- Integer[] localeWeekdayList = DateHelper.getLocaleWeekdayList();
+ Integer[] localeWeekdayList = DateUtils.getLocaleWeekdayList();
for (int j = 0; j < localeWeekdayList.length; j++)
{
rect.set(0, 0, baseSize, baseSize);
rect.offset(prevRect.left, prevRect.top + baseSize * j);
- int i = DateHelper.javaWeekdayToLoopWeekday(localeWeekdayList[j]);
- if(values != null)
- drawMarker(canvas, rect, values[i]);
+ int i = DateUtils.javaWeekdayToLoopWeekday(localeWeekdayList[j]);
+ if (values != null) drawMarker(canvas, rect, values[i]);
rect.offset(0, rowHeight);
}
@@ -255,19 +215,12 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
{
Date time = date.getTime();
- canvas.drawText(dfMonth.format(time), rect.centerX(), rect.centerY() - 0.1f * em, pText);
-
- if(date.get(Calendar.MONTH) == 1)
- canvas.drawText(dfYear.format(time), rect.centerX(), rect.centerY() + 0.9f * em, pText);
- }
-
- private void drawMarker(Canvas canvas, RectF rect, Integer value)
- {
- float padding = rect.height() * 0.2f;
- float radius = (rect.height() - 2 * padding) / 2.0f / 4.0f * Math.min(value, 4);
+ canvas.drawText(dfMonth.format(time), rect.centerX(),
+ rect.centerY() - 0.1f * em, pText);
- pGraph.setColor(colors[Math.min(3, Math.max(0, value - 1))]);
- canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
+ if (date.get(Calendar.MONTH) == 1)
+ canvas.drawText(dfYear.format(time), rect.centerX(),
+ rect.centerY() + 0.9f * em, pText);
}
private void drawGrid(Canvas canvas, RectF rGrid)
@@ -279,12 +232,14 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
pText.setColor(textColor);
pGrid.setColor(gridColor);
- for (String day : DateHelper.getLocaleDayNames(Calendar.SHORT)) {
+ for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
+ {
canvas.drawText(day, rGrid.right - columnWidth,
- rGrid.top + rowHeight / 2 + 0.25f * em, pText);
+ rGrid.top + rowHeight / 2 + 0.25f * em, pText);
pGrid.setStrokeWidth(1f);
- canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
+ canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
+ pGrid);
rGrid.offset(0, rowHeight);
}
@@ -292,9 +247,83 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
- public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ private void drawMarker(Canvas canvas, RectF rect, Integer value)
{
- this.isBackgroundTransparent = isBackgroundTransparent;
- createColors();
+ float padding = rect.height() * 0.2f;
+ // maximal allowed mark radius
+ float maxRadius = (rect.height() - 2 * padding) / 2.0f;
+ // the real mark radius is scaled down by a factor depending on the maximal frequency
+ float scale = 1.0f/maxFreq * value;
+ float radius = maxRadius * scale;
+
+ int colorIndex = Math.round((colors.length-1) * scale);
+ pGraph.setColor(colors[colorIndex]);
+ canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
+ }
+
+ private float getMaxMonthWidth()
+ {
+ float maxMonthWidth = 0;
+ GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
+
+ for (int i = 0; i < 12; i++)
+ {
+ day.set(Calendar.MONTH, i);
+ float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
+ maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
+ }
+
+ return maxMonthWidth;
+ }
+
+ private void init()
+ {
+ initPaints();
+ initColors();
+ initDateFormats();
+ initRects();
+ }
+
+ private void initColors()
+ {
+ StyledResources res = new StyledResources(getContext());
+ textColor = res.getColor(R.attr.mediumContrastTextColor);
+ gridColor = res.getColor(R.attr.lowContrastTextColor);
+
+ colors = new int[4];
+ colors[0] = gridColor;
+ colors[3] = primaryColor;
+ colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f);
+ colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f);
+ }
+
+ private void initDateFormats()
+ {
+ dfMonth = DateFormats.fromSkeleton("MMM");
+ dfYear = DateFormats.fromSkeleton("yyyy");
+ }
+
+ private void initRects()
+ {
+ rect = new RectF();
+ prevRect = new RectF();
+ }
+
+ public void populateWithRandomData()
+ {
+ GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ Random rand = new Random();
+ frequency.clear();
+
+ for (int i = 0; i < 40; i++)
+ {
+ Integer values[] = new Integer[7];
+ for (int j = 0; j < 7; j++)
+ values[j] = rand.nextInt(5);
+
+ frequency.put(date.getTimeInMillis(), values);
+ date.add(Calendar.MONTH, -1);
+ }
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java
similarity index 90%
rename from app/src/main/java/org/isoron/uhabits/views/HabitDataView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java
index b1e239d5e..cd74cf15c 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java
@@ -17,15 +17,13 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
+package org.isoron.uhabits.activities.common.views;
import org.isoron.uhabits.models.Habit;
-public interface HabitDataView
+public interface HabitChart
{
void setHabit(Habit habit);
void refreshData();
-
- void postInvalidate();
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java
similarity index 58%
rename from app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java
index 96899585e..620a2d59f 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java
@@ -17,185 +17,169 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.helpers.DateHelper;
-import org.isoron.uhabits.helpers.UIHelper;
-import org.isoron.uhabits.models.Habit;
-import org.isoron.uhabits.tasks.BaseTask;
-import org.isoron.uhabits.tasks.ToggleRepetitionTask;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.Random;
-
-public class HabitHistoryView extends ScrollableDataView implements HabitDataView,
- ToggleRepetitionTask.Listener
+package org.isoron.uhabits.activities.common.views;
+
+import android.content.*;
+import android.graphics.*;
+import android.graphics.Paint.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+
+import java.text.*;
+import java.util.*;
+
+import static org.isoron.uhabits.models.Checkmark.*;
+
+public class HistoryChart extends ScrollableChart
{
- private Habit habit;
private int[] checkmarks;
+
private Paint pSquareBg, pSquareFg, pTextHeader;
+
private float squareSpacing;
private float squareTextOffset;
+
private float headerTextOffset;
private float columnWidth;
+
private float columnHeight;
+
private int nColumns;
private SimpleDateFormat dfMonth;
+
private SimpleDateFormat dfYear;
private Calendar baseDate;
+
private int nDays;
- /** 0-based-position of today in the column */
+
+ /**
+ * 0-based-position of today in the column
+ */
private int todayPositionInColumn;
+
private int colors[];
+
private RectF baseLocation;
+
private int primaryColor;
private boolean isBackgroundTransparent;
+
private int textColor;
+
private int reverseTextColor;
+
private boolean isEditable;
- public HabitHistoryView(Context context)
+ private String previousMonth;
+
+ private String previousYear;
+
+ private float headerOverflow = 0;
+
+ @NonNull
+ private Controller controller;
+
+ public HistoryChart(Context context)
{
super(context);
init();
}
- public HabitHistoryView(Context context, AttributeSet attrs)
+ public HistoryChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
- public void setHabit(Habit habit)
+ @Override
+ public void onLongPress(MotionEvent e)
{
- this.habit = habit;
- createColors();
+ onSingleTapUp(e);
}
- private void init()
+ @Override
+ public boolean onSingleTapUp(MotionEvent e)
{
- createColors();
- createPaints();
-
- isEditable = false;
- checkmarks = new int[0];
- primaryColor = ColorHelper.getColor(getContext(), 7);
- dfMonth = DateHelper.getDateFormat("MMM");
- dfYear = DateHelper.getDateFormat("yyyy");
+ if (!isEditable) return false;
- baseLocation = new RectF();
- }
+ performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
- private void updateDate()
- {
- baseDate = DateHelper.getStartOfTodayCalendar();
- baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
+ int pointerId = e.getPointerId(0);
+ float x = e.getX(pointerId);
+ float y = e.getY(pointerId);
- nDays = (nColumns - 1) * 7;
- int realWeekday = DateHelper.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK);
- todayPositionInColumn = (7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7;
+ final Long timestamp = positionToTimestamp(x, y);
+ if (timestamp == null) return false;
- baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
- baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn);
- }
+ int offset = timestampToOffset(timestamp);
+ if (offset < checkmarks.length)
+ {
+ boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY;
+ checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY);
+ }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- setMeasuredDimension(width, height);
+ controller.onToggleCheckmark(timestamp);
+ postInvalidate();
+ return true;
}
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
+ public void populateWithRandomData()
{
- if(height < 8) height = 200;
- float baseSize = height / 8.0f;
- setScrollerBucketSize((int) baseSize);
-
- squareSpacing = UIHelper.dpToPixels(getContext(), 1.0f);
- float maxTextSize = getResources().getDimension(R.dimen.regularTextSize);
- float textSize = height * 0.06f;
- textSize = Math.min(textSize, maxTextSize);
-
- pSquareFg.setTextSize(textSize);
- pTextHeader.setTextSize(textSize);
- squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
- headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
+ Random random = new Random();
+ checkmarks = new int[100];
- float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset;
- float horizontalPadding = getPaddingRight() + getPaddingLeft();
+ for (int i = 0; i < 100; i++)
+ if (random.nextFloat() < 0.3) checkmarks[i] = 2;
- columnWidth = baseSize;
- columnHeight = 8 * baseSize;
- nColumns = (int)((width - rightLabelWidth - horizontalPadding) / baseSize) + 1;
+ for (int i = 0; i < 100 - 7; i++)
+ {
+ int count = 0;
+ for (int j = 0; j < 7; j++)
+ if (checkmarks[i + j] != 0) count++;
- updateDate();
+ if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
+ }
}
- private float getWeekdayLabelWidth()
+ public void setCheckmarks(int[] checkmarks)
{
- float width = 0;
-
- for(String w : DateHelper.getLocaleDayNames(Calendar.SHORT))
- width = Math.max(width, pSquareFg.measureText(w));
-
- return width;
+ this.checkmarks = checkmarks;
+ postInvalidate();
}
- private void createColors()
+ public void setColor(int color)
{
- if(habit != null)
- this.primaryColor = ColorHelper.getColor(getContext(), habit.color);
+ this.primaryColor = color;
+ initColors();
+ postInvalidate();
+ }
- if(isBackgroundTransparent)
- primaryColor = ColorHelper.setMinValue(primaryColor, 0.75f);
+ public void setController(@NonNull Controller controller)
+ {
+ this.controller = controller;
+ }
- int red = Color.red(primaryColor);
- int green = Color.green(primaryColor);
- int blue = Color.blue(primaryColor);
+ public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ {
+ this.isBackgroundTransparent = isBackgroundTransparent;
+ initColors();
+ }
- if(isBackgroundTransparent)
- {
- colors = new int[3];
- colors[0] = Color.argb(16, 255, 255, 255);
- colors[1] = Color.argb(128, red, green, blue);
- colors[2] = primaryColor;
- textColor = Color.WHITE;
- reverseTextColor = Color.WHITE;
- }
- else
- {
- colors = new int[3];
- colors[0] = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor);
- colors[1] = Color.argb(127, red, green, blue);
- colors[2] = primaryColor;
- textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
- reverseTextColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastReverseTextColor);
- }
+ public void setIsEditable(boolean isEditable)
+ {
+ this.isEditable = isEditable;
}
- protected void createPaints()
+ protected void initPaints()
{
pTextHeader = new Paint();
pTextHeader.setTextAlign(Align.LEFT);
@@ -208,48 +192,13 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
pSquareFg.setTextAlign(Align.CENTER);
}
- public void refreshData()
- {
- if(isInEditMode())
- generateRandomData();
- else
- {
- if(habit == null) return;
- checkmarks = habit.checkmarks.getAllValues();
- }
-
- updateDate();
- postInvalidate();
- }
-
- private void generateRandomData()
- {
- Random random = new Random();
- checkmarks = new int[100];
-
- for(int i = 0; i < 100; i++)
- if(random.nextFloat() < 0.3) checkmarks[i] = 2;
-
- for(int i = 0; i < 100 - 7; i++)
- {
- int count = 0;
- for (int j = 0; j < 7; j++)
- if(checkmarks[i + j] != 0)
- count++;
-
- if(count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
- }
- }
-
- private String previousMonth;
- private String previousYear;
-
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
- baseLocation.set(0, 0, columnWidth - squareSpacing, columnWidth - squareSpacing);
+ baseLocation.set(0, 0, columnWidth - squareSpacing,
+ columnWidth - squareSpacing);
baseLocation.offset(getPaddingLeft(), getPaddingTop());
headerOverflow = 0;
@@ -263,108 +212,189 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
for (int column = 0; column < nColumns - 1; column++)
{
drawColumn(canvas, baseLocation, currentDate, column);
- baseLocation.offset(columnWidth, - columnHeight);
+ baseLocation.offset(columnWidth, -columnHeight);
}
drawAxis(canvas, baseLocation);
}
- private void drawColumn(Canvas canvas, RectF location, GregorianCalendar date, int column)
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
- drawColumnHeader(canvas, location, date);
- location.offset(0, columnWidth);
-
- for (int j = 0; j < 7; j++)
- {
- if (!(column == nColumns - 2 && getDataOffset() == 0 && j > todayPositionInColumn))
- {
- int checkmarkOffset = getDataOffset() * 7 + nDays - 7 * (column + 1) +
- todayPositionInColumn - j;
- drawSquare(canvas, location, date, checkmarkOffset);
- }
-
- date.add(Calendar.DAY_OF_MONTH, 1);
- location.offset(0, columnWidth);
- }
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
}
- private void drawSquare(Canvas canvas, RectF location, GregorianCalendar date,
- int checkmarkOffset)
+ @Override
+ protected void onSizeChanged(int width,
+ int height,
+ int oldWidth,
+ int oldHeight)
{
- if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
- else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]);
+ if (height < 8) height = 200;
+ float baseSize = height / 8.0f;
+ setScrollerBucketSize((int) baseSize);
- pSquareFg.setColor(reverseTextColor);
- canvas.drawRect(location, pSquareBg);
- String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
- canvas.drawText(text, location.centerX(), location.centerY() + squareTextOffset, pSquareFg);
+ squareSpacing = InterfaceUtils.dpToPixels(getContext(), 1.0f);
+ float maxTextSize =
+ getResources().getDimension(R.dimen.regularTextSize);
+ float textSize = height * 0.06f;
+ textSize = Math.min(textSize, maxTextSize);
+
+ pSquareFg.setTextSize(textSize);
+ pTextHeader.setTextSize(textSize);
+ squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
+ headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
+
+ float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset;
+ float horizontalPadding = getPaddingRight() + getPaddingLeft();
+
+ columnWidth = baseSize;
+ columnHeight = 8 * baseSize;
+ nColumns =
+ (int) ((width - rightLabelWidth - horizontalPadding) / baseSize) +
+ 1;
+
+ updateDate();
}
private void drawAxis(Canvas canvas, RectF location)
{
float verticalOffset = pTextHeader.getFontSpacing() * 0.4f;
- for (String day : DateHelper.getLocaleDayNames(Calendar.SHORT))
+ for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
{
location.offset(0, columnWidth);
canvas.drawText(day, location.left + headerTextOffset,
- location.centerY() + verticalOffset, pTextHeader);
+ location.centerY() + verticalOffset, pTextHeader);
}
}
- private float headerOverflow = 0;
+ private void drawColumn(Canvas canvas,
+ RectF location,
+ GregorianCalendar date,
+ int column)
+ {
+ drawColumnHeader(canvas, location, date);
+ location.offset(0, columnWidth);
- private void drawColumnHeader(Canvas canvas, RectF location, GregorianCalendar date)
+ for (int j = 0; j < 7; j++)
+ {
+ if (!(column == nColumns - 2 && getDataOffset() == 0 &&
+ j > todayPositionInColumn))
+ {
+ int checkmarkOffset =
+ getDataOffset() * 7 + nDays - 7 * (column + 1) +
+ todayPositionInColumn - j;
+ drawSquare(canvas, location, date, checkmarkOffset);
+ }
+
+ date.add(Calendar.DAY_OF_MONTH, 1);
+ location.offset(0, columnWidth);
+ }
+ }
+
+ private void drawColumnHeader(Canvas canvas,
+ RectF location,
+ GregorianCalendar date)
{
String month = dfMonth.format(date.getTime());
String year = dfYear.format(date.getTime());
String text = null;
- if (!month.equals(previousMonth))
- text = previousMonth = month;
- else if(!year.equals(previousYear))
- text = previousYear = year;
+ if (!month.equals(previousMonth)) text = previousMonth = month;
+ else if (!year.equals(previousYear)) text = previousYear = year;
- if(text != null)
+ if (text != null)
{
- canvas.drawText(text, location.left + headerOverflow, location.bottom - headerTextOffset, pTextHeader);
- headerOverflow += pTextHeader.measureText(text) + columnWidth * 0.2f;
+ canvas.drawText(text, location.left + headerOverflow,
+ location.bottom - headerTextOffset, pTextHeader);
+ headerOverflow +=
+ pTextHeader.measureText(text) + columnWidth * 0.2f;
}
headerOverflow = Math.max(0, headerOverflow - columnWidth);
}
- public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ private void drawSquare(Canvas canvas,
+ RectF location,
+ GregorianCalendar date,
+ int checkmarkOffset)
{
- this.isBackgroundTransparent = isBackgroundTransparent;
- createColors();
+ if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
+ else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]);
+
+ pSquareFg.setColor(reverseTextColor);
+ canvas.drawRect(location, pSquareBg);
+ String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
+ canvas.drawText(text, location.centerX(),
+ location.centerY() + squareTextOffset, pSquareFg);
}
- @Override
- public void onLongPress(MotionEvent e)
+ private float getWeekdayLabelWidth()
{
- onSingleTapUp(e);
+ float width = 0;
+
+ for (String w : DateUtils.getLocaleDayNames(Calendar.SHORT))
+ width = Math.max(width, pSquareFg.measureText(w));
+
+ return width;
}
- @Override
- public boolean onSingleTapUp(MotionEvent e)
+ private void init()
{
- if(!isEditable) return false;
+ isEditable = false;
+ checkmarks = new int[0];
+ controller = new Controller() {};
- performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+ initColors();
+ initPaints();
+ initDateFormats();
+ initRects();
+ }
- int pointerId = e.getPointerId(0);
- float x = e.getX(pointerId);
- float y = e.getY(pointerId);
+ private void initColors()
+ {
+ StyledResources res = new StyledResources(getContext());
- final Long timestamp = positionToTimestamp(x, y);
- if(timestamp == null) return false;
+ if (isBackgroundTransparent)
+ primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f);
- ToggleRepetitionTask task = new ToggleRepetitionTask(habit, timestamp);
- task.setListener(this);
- task.execute();
+ int red = Color.red(primaryColor);
+ int green = Color.green(primaryColor);
+ int blue = Color.blue(primaryColor);
- return true;
+ if (isBackgroundTransparent)
+ {
+ colors = new int[3];
+ colors[0] = Color.argb(16, 255, 255, 255);
+ colors[1] = Color.argb(128, red, green, blue);
+ colors[2] = primaryColor;
+ textColor = Color.WHITE;
+ reverseTextColor = Color.WHITE;
+ }
+ else
+ {
+ colors = new int[3];
+ colors[0] = res.getColor(R.attr.lowContrastTextColor);
+ colors[1] = Color.argb(127, red, green, blue);
+ colors[2] = primaryColor;
+ textColor = res.getColor(R.attr.mediumContrastTextColor);
+ reverseTextColor =
+ res.getColor(R.attr.highContrastReverseTextColor);
+ }
+ }
+
+ private void initDateFormats()
+ {
+ dfMonth = DateFormats.fromSkeleton("MMM");
+ dfYear = DateFormats.fromSkeleton("yyyy");
+ }
+
+ private void initRects()
+ {
+ baseLocation = new RectF();
}
private Long positionToTimestamp(float x, float y)
@@ -372,41 +402,44 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
int col = (int) (x / columnWidth);
int row = (int) (y / columnWidth);
- if(row == 0) return null;
- if(col == nColumns - 1) return null;
+ if (row == 0) return null;
+ if (col == nColumns - 1) return null;
int offset = col * 7 + (row - 1);
Calendar date = (Calendar) baseDate.clone();
date.add(Calendar.DAY_OF_YEAR, offset);
- if(DateHelper.getStartOfDay(date.getTimeInMillis()) > DateHelper.getStartOfToday())
- return null;
+ if (DateUtils.getStartOfDay(date.getTimeInMillis()) >
+ DateUtils.getStartOfToday()) return null;
return date.getTimeInMillis();
}
- public void setIsEditable(boolean isEditable)
+ private int timestampToOffset(Long timestamp)
{
- this.isEditable = isEditable;
+ Long day = DateUtils.millisecondsInOneDay;
+ Long today = DateUtils.getStartOfToday();
+
+ return (int) ((today - timestamp) / day);
}
- @Override
- public void onToggleRepetitionFinished()
+ private void updateDate()
{
- new BaseTask()
- {
- @Override
- protected void doInBackground()
- {
- refreshData();
- }
+ baseDate = DateUtils.getStartOfTodayCalendar();
+ baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
- @Override
- protected void onPostExecute(Void aVoid)
- {
- invalidate();
- super.onPostExecute(null);
- }
- }.execute();
+ nDays = (nColumns - 1) * 7;
+ int realWeekday =
+ DateUtils.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK);
+ todayPositionInColumn =
+ (7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7;
+
+ baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
+ baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn);
+ }
+
+ public interface Controller
+ {
+ default void onToggleCheckmark(long timestamp) {}
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java
similarity index 61%
rename from app/src/main/java/org/isoron/uhabits/views/RingView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java
index cac641a22..e093a9f2b 100644
--- a/app/src/main/java/org/isoron/uhabits/views/RingView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java
@@ -17,49 +17,55 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.RectF;
-import android.support.annotation.Nullable;
-import android.text.TextPaint;
-import android.util.AttributeSet;
-import android.view.View;
-
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.helpers.UIHelper;
+package org.isoron.uhabits.activities.common.views;
+
+import android.content.*;
+import android.graphics.*;
+import android.support.annotation.*;
+import android.text.*;
+import android.util.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.utils.*;
+
+import static org.isoron.uhabits.utils.AttributeSetUtils.*;
+import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class RingView extends View
{
public static final PorterDuffXfermode XFERMODE_CLEAR =
- new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
+ new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private int color;
+
private float precision;
+
private float percentage;
+
private int diameter;
+
private float thickness;
private RectF rect;
+
private TextPaint pRing;
private Integer backgroundColor;
+
private Integer inactiveColor;
private float em;
+
private String text;
+
private float textSize;
+
private boolean enableFontAwesome;
@Nullable
private Bitmap drawingCache;
+
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
@@ -70,55 +76,56 @@ public class RingView extends View
percentage = 0.0f;
precision = 0.01f;
- color = ColorHelper.CSV_PALETTE[0];
- thickness = UIHelper.dpToPixels(getContext(), 2);
+ color = ColorUtils.getAndroidTestColor(0);
+ thickness = dpToPixels(getContext(), 2);
text = "";
textSize = context.getResources().getDimension(R.dimen.smallTextSize);
init();
}
- public RingView(Context context, AttributeSet attrs)
+ public RingView(Context ctx, AttributeSet attrs)
{
- super(context, attrs);
-
- percentage = UIHelper.getFloatAttribute(context, attrs, "percentage", 0);
- precision = UIHelper.getFloatAttribute(context, attrs, "precision", 0.01f);
+ super(ctx, attrs);
- color = UIHelper.getColorAttribute(context, attrs, "color", 0);
- backgroundColor = UIHelper.getColorAttribute(context, attrs, "backgroundColor", null);
- inactiveColor = UIHelper.getColorAttribute(context, attrs, "inactiveColor", null);
+ percentage = getFloatAttribute(ctx, attrs, "percentage", 0);
+ precision = getFloatAttribute(ctx, attrs, "precision", 0.01f);
- thickness = UIHelper.getFloatAttribute(context, attrs, "thickness", 0);
- thickness = UIHelper.dpToPixels(context, thickness);
+ color = getColorAttribute(ctx, attrs, "color", 0);
+ backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null);
+ inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null);
- float defaultTextSize = context.getResources().getDimension(R.dimen.smallTextSize);
- textSize = UIHelper.getFloatAttribute(context, attrs, "textSize", defaultTextSize);
- textSize = UIHelper.spToPixels(context, textSize);
+ thickness = getFloatAttribute(ctx, attrs, "thickness", 0);
+ thickness = dpToPixels(ctx, thickness);
- text = UIHelper.getAttribute(context, attrs, "text", "");
+ float defaultTextSize =
+ ctx.getResources().getDimension(R.dimen.smallTextSize);
+ textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize);
+ textSize = spToPixels(ctx, textSize);
+ text = AttributeSetUtils.getAttribute(ctx, attrs, "text", "");
- enableFontAwesome = UIHelper.getBooleanAttribute(context, attrs, "enableFontAwesome", false);
+ enableFontAwesome = AttributeSetUtils.getBooleanAttribute(ctx, attrs,
+ "enableFontAwesome", false);
init();
}
- public void setColor(int color)
+ @Override
+ public void setBackgroundColor(int backgroundColor)
{
- this.color = color;
+ this.backgroundColor = backgroundColor;
postInvalidate();
}
- public void setTextSize(float textSize)
+ public void setColor(int color)
{
- this.textSize = textSize;
+ this.color = color;
+ postInvalidate();
}
- @Override
- public void setBackgroundColor(int backgroundColor)
+ public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
{
- this.backgroundColor = backgroundColor;
- postInvalidate();
+ this.isTransparencyEnabled = isTransparencyEnabled;
}
public void setPercentage(float percentage)
@@ -133,64 +140,21 @@ public class RingView extends View
postInvalidate();
}
- public void setThickness(float thickness)
- {
- this.thickness = thickness;
- postInvalidate();
- }
-
public void setText(String text)
{
this.text = text;
postInvalidate();
}
- private void init()
- {
- pRing = new TextPaint();
- pRing.setAntiAlias(true);
- pRing.setColor(color);
- pRing.setTextAlign(Paint.Align.CENTER);
-
- if(backgroundColor == null)
- backgroundColor = UIHelper.getStyledColor(getContext(), R.attr.cardBackgroundColor);
-
- if(inactiveColor == null)
- inactiveColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastTextColor);
-
- inactiveColor = ColorHelper.setAlpha(inactiveColor, 0.1f);
-
- rect = new RectF();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- diameter = Math.min(height, width);
-
- pRing.setTextSize(textSize);
- em = pRing.measureText("M");
-
- setMeasuredDimension(diameter, diameter);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh)
+ public void setTextSize(float textSize)
{
- super.onSizeChanged(w, h, oldw, oldh);
-
- if(isTransparencyEnabled) reallocateCache();
+ this.textSize = textSize;
}
- private void reallocateCache()
+ public void setThickness(float thickness)
{
- if (drawingCache != null) drawingCache.recycle();
- drawingCache = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
- cacheCanvas = new Canvas(drawingCache);
+ this.thickness = thickness;
+ postInvalidate();
}
@Override
@@ -199,9 +163,9 @@ public class RingView extends View
super.onDraw(canvas);
Canvas activeCanvas;
- if(isTransparencyEnabled)
+ if (isTransparencyEnabled)
{
- if(drawingCache == null) reallocateCache();
+ if (drawingCache == null) reallocateCache();
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
}
@@ -220,12 +184,10 @@ public class RingView extends View
pRing.setColor(inactiveColor);
activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing);
- if(thickness > 0)
+ if (thickness > 0)
{
- if(isTransparencyEnabled)
- pRing.setXfermode(XFERMODE_CLEAR);
- else
- pRing.setColor(backgroundColor);
+ if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR);
+ else pRing.setColor(backgroundColor);
rect.inset(thickness, thickness);
activeCanvas.drawArc(rect, 0, 360, true, pRing);
@@ -233,16 +195,63 @@ public class RingView extends View
pRing.setColor(color);
pRing.setTextSize(textSize);
- if(enableFontAwesome) pRing.setTypeface(UIHelper.getFontAwesome(getContext()));
- activeCanvas.drawText(text, rect.centerX(), rect.centerY() + 0.4f * em, pRing);
+ if (enableFontAwesome)
+ pRing.setTypeface(getFontAwesome(getContext()));
+ activeCanvas.drawText(text, rect.centerX(),
+ rect.centerY() + 0.4f * em, pRing);
}
- if(activeCanvas != canvas)
- canvas.drawBitmap(drawingCache, 0, 0, null);
+ if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
- public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
- this.isTransparencyEnabled = isTransparencyEnabled;
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ diameter = Math.min(height, width);
+
+ pRing.setTextSize(textSize);
+ em = pRing.measureText("M");
+
+ setMeasuredDimension(diameter, diameter);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh)
+ {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ if (isTransparencyEnabled) reallocateCache();
+ }
+
+ private void init()
+ {
+ pRing = new TextPaint();
+ pRing.setAntiAlias(true);
+ pRing.setColor(color);
+ pRing.setTextAlign(Paint.Align.CENTER);
+
+ StyledResources res = new StyledResources(getContext());
+
+ if (backgroundColor == null)
+ backgroundColor = res.getColor(R.attr.cardBackgroundColor);
+
+ if (inactiveColor == null)
+ inactiveColor = res.getColor(R.attr.highContrastTextColor);
+
+ inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f);
+
+ rect = new RectF();
+ }
+
+ private void reallocateCache()
+ {
+ if (drawingCache != null) drawingCache.recycle();
+ drawingCache =
+ Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
+ cacheCanvas = new Canvas(drawingCache);
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java
similarity index 60%
rename from app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java
index 6258e7b13..902959684 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java
@@ -17,206 +17,136 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.RectF;
-import android.support.annotation.Nullable;
-import android.util.AttributeSet;
-
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.helpers.DateHelper;
-import org.isoron.uhabits.helpers.UIHelper;
-import org.isoron.uhabits.models.Habit;
-import org.isoron.uhabits.models.Score;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.Random;
-
-public class HabitScoreView extends ScrollableDataView implements HabitDataView
+package org.isoron.uhabits.activities.common.views;
+
+import android.content.*;
+import android.graphics.*;
+import android.support.annotation.*;
+import android.util.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import java.text.*;
+import java.util.*;
+
+import static org.isoron.uhabits.utils.InterfaceUtils.*;
+
+public class ScoreChart extends ScrollableChart
{
- public static final PorterDuffXfermode XFERMODE_CLEAR =
- new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
- public static final PorterDuffXfermode XFERMODE_SRC =
- new PorterDuffXfermode(PorterDuff.Mode.SRC);
+ private static final PorterDuffXfermode XFERMODE_CLEAR =
+ new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
- public static int DEFAULT_BUCKET_SIZES[] = { 1, 7, 31, 92, 365 };
+ private static final PorterDuffXfermode XFERMODE_SRC =
+ new PorterDuffXfermode(PorterDuff.Mode.SRC);
private Paint pGrid;
+
private float em;
- private Habit habit;
private SimpleDateFormat dfMonth;
+
private SimpleDateFormat dfDay;
+
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
+
private RectF rect, prevRect;
+
private int baseSize;
+
private int paddingTop;
private float columnWidth;
+
private int columnHeight;
+
private int nColumns;
private int textColor;
+
private int gridColor;
@Nullable
- private int[] scores;
+ private List scores;
private int primaryColor;
+
+ @Deprecated
private int bucketSize = 7;
- private int footerHeight;
+
private int backgroundColor;
private Bitmap drawingCache;
+
private Canvas cacheCanvas;
+
private boolean isTransparencyEnabled;
- public HabitScoreView(Context context)
+ private int skipYear = 0;
+
+ private String previousYearText;
+
+ private String previousMonthText;
+
+ public ScoreChart(Context context)
{
super(context);
init();
}
- public HabitScoreView(Context context, AttributeSet attrs)
+ public ScoreChart(Context context, AttributeSet attrs)
{
super(context, attrs);
- this.primaryColor = ColorHelper.getColor(getContext(), 7);
init();
}
- public void setHabit(Habit habit)
- {
- this.habit = habit;
- createColors();
- }
-
- private void init()
- {
- createPaints();
- createColors();
-
- dfYear = DateHelper.getDateFormat("yyyy");
- dfMonth = DateHelper.getDateFormat("MMM");
- dfDay = DateHelper.getDateFormat("d");
-
- rect = new RectF();
- prevRect = new RectF();
- }
-
- private void createColors()
- {
- if(habit != null)
- this.primaryColor = ColorHelper.getColor(getContext(), habit.color);
-
- textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
- gridColor = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor);
- backgroundColor = UIHelper.getStyledColor(getContext(), R.attr.cardBackgroundColor);
- }
-
- protected void createPaints()
+ public void populateWithRandomData()
{
- pText = new Paint();
- pText.setAntiAlias(true);
+ Random random = new Random();
+ scores = new LinkedList<>();
- pGraph = new Paint();
- pGraph.setTextAlign(Paint.Align.CENTER);
- pGraph.setAntiAlias(true);
+ int previous = Score.MAX_VALUE / 2;
+ long timestamp = DateUtils.getStartOfToday();
+ long day = DateUtils.millisecondsInOneDay;
- pGrid = new Paint();
- pGrid.setAntiAlias(true);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- setMeasuredDimension(width, height);
+ for (int i = 1; i < 100; i++)
+ {
+ int step = Score.MAX_VALUE / 10;
+ int current = previous + random.nextInt(step * 2) - step;
+ current = Math.max(0, Math.min(Score.MAX_VALUE, current));
+ scores.add(new Score(timestamp, current));
+ previous = current;
+ timestamp -= day;
+ }
}
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
+ @Deprecated
+ public void setBucketSize(int bucketSize)
{
- if(height < 9) height = 200;
-
- float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize);
- float textSize = height * 0.06f;
- pText.setTextSize(Math.min(textSize, maxTextSize));
- em = pText.getFontSpacing();
-
- footerHeight = (int)(3 * em);
- paddingTop = (int) (em);
-
- baseSize = (height - footerHeight - paddingTop) / 8;
- setScrollerBucketSize(baseSize);
-
- columnWidth = baseSize;
- columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
- columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
-
- nColumns = (int) (width / columnWidth);
- columnWidth = (float) width / nColumns;
-
- columnHeight = 8 * baseSize;
-
- float minStrokeWidth = UIHelper.dpToPixels(getContext(), 1);
- pGraph.setTextSize(baseSize * 0.5f);
- pGraph.setStrokeWidth(baseSize * 0.1f);
- pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
-
- if(isTransparencyEnabled)
- initCache(width, height);
+ this.bucketSize = bucketSize;
+ postInvalidate();
}
- private void initCache(int width, int height)
+ public void setIsTransparencyEnabled(boolean enabled)
{
- if (drawingCache != null) drawingCache.recycle();
- drawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- cacheCanvas = new Canvas(drawingCache);
+ this.isTransparencyEnabled = enabled;
+ initColors();
+ requestLayout();
}
- public void refreshData()
+ public void setColor(int primaryColor)
{
- if(isInEditMode())
- generateRandomData();
- else
- {
- if (habit == null) return;
- scores = habit.scores.getAllValues(bucketSize);
- }
-
+ this.primaryColor = primaryColor;
postInvalidate();
}
- public void setBucketSize(int bucketSize)
- {
- this.bucketSize = bucketSize;
- }
-
- private void generateRandomData()
+ public void setScores(@NonNull List scores)
{
- Random random = new Random();
- scores = new int[100];
- scores[0] = Score.MAX_VALUE / 2;
-
- for(int i = 1; i < 100; i++)
- {
- int step = Score.MAX_VALUE / 10;
- scores[i] = scores[i - 1] + random.nextInt(step * 2) - step;
- scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i]));
- }
+ this.scores = scores;
+ postInvalidate();
}
@Override
@@ -225,9 +155,9 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
super.onDraw(canvas);
Canvas activeCanvas;
- if(isTransparencyEnabled)
+ if (isTransparencyEnabled)
{
- if(drawingCache == null) initCache(getWidth(), getHeight());
+ if (drawingCache == null) initCache(getWidth(), getHeight());
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
@@ -237,7 +167,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
activeCanvas = canvas;
}
- if (habit == null || scores == null) return;
+ if (scores == null) return;
rect.set(0, 0, nColumns * columnWidth, columnHeight);
rect.offset(0, paddingTop);
@@ -252,23 +182,20 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
previousYearText = "";
skipYear = 0;
- long currentDate = DateHelper.getStartOfToday();
-
- for(int k = 0; k < nColumns + getDataOffset() - 1; k++)
- currentDate -= bucketSize * DateHelper.millisecondsInOneDay;
-
for (int k = 0; k < nColumns; k++)
{
- int score = 0;
int offset = nColumns - k - 1 + getDataOffset();
- if(offset < scores.length) score = scores[offset];
+ if (offset >= scores.size()) continue;
+
+ int score = scores.get(offset).getValue();
+ long timestamp = scores.get(offset).getTimestamp();
double relativeScore = ((double) score) / Score.MAX_VALUE;
int height = (int) (columnHeight * relativeScore);
rect.set(0, 0, baseSize, baseSize);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
- paddingTop + columnHeight - height - baseSize / 2);
+ paddingTop + columnHeight - height - baseSize / 2);
if (!prevRect.isEmpty())
{
@@ -282,18 +209,54 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(k * columnWidth, paddingTop);
- drawFooter(activeCanvas, rect, currentDate);
-
- currentDate += bucketSize * DateHelper.millisecondsInOneDay;
+ drawFooter(activeCanvas, rect, timestamp);
}
- if(activeCanvas != canvas)
- canvas.drawBitmap(drawingCache, 0, 0, null);
+ if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
- private int skipYear = 0;
- private String previousYearText;
- private String previousMonthText;
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onSizeChanged(int width,
+ int height,
+ int oldWidth,
+ int oldHeight)
+ {
+ if (height < 9) height = 200;
+
+ float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize);
+ float textSize = height * 0.06f;
+ pText.setTextSize(Math.min(textSize, maxTextSize));
+ em = pText.getFontSpacing();
+
+ int footerHeight = (int) (3 * em);
+ paddingTop = (int) (em);
+
+ baseSize = (height - footerHeight - paddingTop) / 8;
+ columnWidth = baseSize;
+ columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
+ columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
+
+ nColumns = (int) (width / columnWidth);
+ columnWidth = (float) width / nColumns;
+ setScrollerBucketSize((int) columnWidth);
+
+ columnHeight = 8 * baseSize;
+
+ float minStrokeWidth = dpToPixels(getContext(), 1);
+ pGraph.setTextSize(baseSize * 0.5f);
+ pGraph.setStrokeWidth(baseSize * 0.1f);
+ pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
+
+ if (isTransparencyEnabled) initCache(width, height);
+ }
private void drawFooter(Canvas canvas, RectF rect, long currentDate)
{
@@ -301,35 +264,36 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
String monthText = dfMonth.format(currentDate);
String dayText = dfDay.format(currentDate);
- GregorianCalendar calendar = DateHelper.getCalendar(currentDate);
+ GregorianCalendar calendar = DateUtils.getCalendar(currentDate);
String text;
int year = calendar.get(Calendar.YEAR);
boolean shouldPrintYear = true;
- if(yearText.equals(previousYearText)) shouldPrintYear = false;
- if(bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
+ if (yearText.equals(previousYearText)) shouldPrintYear = false;
+ if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
- if(skipYear > 0)
+ if (skipYear > 0)
{
skipYear--;
shouldPrintYear = false;
}
- if(shouldPrintYear)
+ if (shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
- canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText);
+ canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f,
+ pText);
skipYear = 1;
}
- if(bucketSize < 365)
+ if (bucketSize < 365)
{
- if(!monthText.equals(previousMonthText))
+ if (!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
@@ -340,11 +304,11 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
}
pText.setTextAlign(Paint.Align.CENTER);
- canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, pText);
+ canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
+ pText);
}
}
-
private void drawGrid(Canvas canvas, RectF rGrid)
{
int nRows = 5;
@@ -356,9 +320,10 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
for (int i = 0; i < nRows; i++)
{
- canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em,
- rGrid.top + 1f * em, pText);
- canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
+ canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)),
+ rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText);
+ canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
+ pGrid);
rGrid.offset(0, rowHeight);
}
@@ -368,13 +333,13 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
{
pGraph.setColor(primaryColor);
- canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(),
- pGraph);
+ canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(),
+ rectTo.centerX(), rectTo.centerY(), pGraph);
}
private void drawMarker(Canvas canvas, RectF rect)
{
- rect.inset(baseSize * 0.15f, baseSize * 0.15f);
+ rect.inset(baseSize * 0.225f, baseSize * 0.225f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
@@ -382,35 +347,34 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
canvas.drawOval(rect, pGraph);
- rect.inset(baseSize * 0.1f, baseSize * 0.1f);
- setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
- canvas.drawOval(rect, pGraph);
+// rect.inset(baseSize * 0.1f, baseSize * 0.1f);
+// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
+// canvas.drawOval(rect, pGraph);
- if(isTransparencyEnabled)
- pGraph.setXfermode(XFERMODE_SRC);
+ if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
}
- public void setIsTransparencyEnabled(boolean enabled)
+ private float getMaxDayWidth()
{
- this.isTransparencyEnabled = enabled;
- createColors();
- requestLayout();
- }
+ float maxDayWidth = 0;
+ GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
- private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
- {
- if(isTransparencyEnabled)
- p.setXfermode(mode);
- else
- p.setColor(color);
+ for (int i = 0; i < 28; i++)
+ {
+ day.set(Calendar.DAY_OF_MONTH, i);
+ float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
+ maxDayWidth = Math.max(maxDayWidth, monthWidth);
+ }
+
+ return maxDayWidth;
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
- GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
+ GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
- for(int i = 0; i < 12; i++)
+ for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
@@ -420,18 +384,61 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
return maxMonthWidth;
}
- private float getMaxDayWidth()
+ private void init()
{
- float maxDayWidth = 0;
- GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
+ initPaints();
+ initColors();
+ initDateFormats();
+ initRects();
+ }
- for(int i = 0; i < 28; i++)
- {
- day.set(Calendar.DAY_OF_MONTH, i);
- float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
- maxDayWidth = Math.max(maxDayWidth, monthWidth);
- }
+ private void initCache(int width, int height)
+ {
+ if (drawingCache != null) drawingCache.recycle();
+ drawingCache =
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ cacheCanvas = new Canvas(drawingCache);
+ }
- return maxDayWidth;
+ private void initColors()
+ {
+ StyledResources res = new StyledResources(getContext());
+
+ primaryColor = Color.BLACK;
+ textColor = res.getColor(R.attr.mediumContrastTextColor);
+ gridColor = res.getColor(R.attr.lowContrastTextColor);
+ backgroundColor = res.getColor(R.attr.cardBackgroundColor);
+ }
+
+ private void initDateFormats()
+ {
+ dfYear = DateFormats.fromSkeleton("yyyy");
+ dfMonth = DateFormats.fromSkeleton("MMM");
+ dfDay = DateFormats.fromSkeleton("d");
+ }
+
+ private void initPaints()
+ {
+ pText = new Paint();
+ pText.setAntiAlias(true);
+
+ pGraph = new Paint();
+ pGraph.setTextAlign(Paint.Align.CENTER);
+ pGraph.setAntiAlias(true);
+
+ pGrid = new Paint();
+ pGrid.setAntiAlias(true);
+ }
+
+ private void initRects()
+ {
+ rect = new RectF();
+ prevRect = new RectF();
+ }
+
+ private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
+ {
+ if (isTransparencyEnabled) p.setXfermode(mode);
+ else p.setColor(color);
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java
similarity index 76%
rename from app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java
index fbae274b7..a762ecf86 100644
--- a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java
@@ -17,52 +17,59 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewParent;
-import android.widget.Scroller;
-
-public abstract class ScrollableDataView extends View implements GestureDetector.OnGestureListener,
- ValueAnimator.AnimatorUpdateListener
+package org.isoron.uhabits.activities.common.views;
+
+import android.animation.*;
+import android.content.*;
+import android.util.*;
+import android.view.*;
+import android.widget.*;
+
+public abstract class ScrollableChart extends View
+ implements GestureDetector.OnGestureListener,
+ ValueAnimator.AnimatorUpdateListener
{
private int dataOffset;
+
private int scrollerBucketSize;
private GestureDetector detector;
+
private Scroller scroller;
+
private ValueAnimator scrollAnimator;
- public ScrollableDataView(Context context)
+ public ScrollableChart(Context context)
{
super(context);
init(context);
}
- public ScrollableDataView(Context context, AttributeSet attrs)
+ public ScrollableChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init(context);
}
- private void init(Context context)
+ public int getDataOffset()
{
- detector = new GestureDetector(context, this);
- scroller = new Scroller(context, null, true);
- scrollAnimator = ValueAnimator.ofFloat(0, 1);
- scrollAnimator.addUpdateListener(this);
+ return dataOffset;
}
@Override
- public boolean onTouchEvent(MotionEvent event)
+ public void onAnimationUpdate(ValueAnimator animation)
{
- return detector.onTouchEvent(event);
+ if (!scroller.isFinished())
+ {
+ scroller.computeScrollOffset();
+ dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
+ postInvalidate();
+ }
+ else
+ {
+ scrollAnimator.cancel();
+ }
}
@Override
@@ -72,30 +79,40 @@ public abstract class ScrollableDataView extends View implements GestureDetector
}
@Override
- public void onShowPress(MotionEvent e)
+ public boolean onFling(MotionEvent e1,
+ MotionEvent e2,
+ float velocityX,
+ float velocityY)
{
+ scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
+ (int) velocityX / 2, 0, 0, 100000, 0, 0);
+ invalidate();
+
+ scrollAnimator.setDuration(scroller.getDuration());
+ scrollAnimator.start();
+ return false;
}
@Override
- public boolean onSingleTapUp(MotionEvent e)
+ public void onLongPress(MotionEvent e)
{
- return false;
+
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
{
- if(scrollerBucketSize == 0)
- return false;
+ if (scrollerBucketSize == 0) return false;
- if(Math.abs(dx) > Math.abs(dy))
+ if (Math.abs(dx) > Math.abs(dy))
{
ViewParent parent = getParent();
- if(parent != null) parent.requestDisallowInterceptTouchEvent(true);
+ if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
}
- scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) -dx, (int) dy, 0);
+ scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(),
+ (int) -dx, (int) dy, 0);
scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
postInvalidate();
@@ -104,46 +121,33 @@ public abstract class ScrollableDataView extends View implements GestureDetector
}
@Override
- public void onLongPress(MotionEvent e)
+ public void onShowPress(MotionEvent e)
{
}
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
+ public boolean onSingleTapUp(MotionEvent e)
{
- scroller.fling(scroller.getCurrX(), scroller.getCurrY(), (int) velocityX / 2, 0, 0, 100000,
- 0, 0);
- invalidate();
-
- scrollAnimator.setDuration(scroller.getDuration());
- scrollAnimator.start();
-
return false;
}
@Override
- public void onAnimationUpdate(ValueAnimator animation)
+ public boolean onTouchEvent(MotionEvent event)
{
- if (!scroller.isFinished())
- {
- scroller.computeScrollOffset();
- dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
- postInvalidate();
- }
- else
- {
- scrollAnimator.cancel();
- }
+ return detector.onTouchEvent(event);
}
- public int getDataOffset()
+ public void setScrollerBucketSize(int scrollerBucketSize)
{
- return dataOffset;
+ this.scrollerBucketSize = scrollerBucketSize;
}
- public void setScrollerBucketSize(int scrollerBucketSize)
+ private void init(Context context)
{
- this.scrollerBucketSize = scrollerBucketSize;
+ detector = new GestureDetector(context, this);
+ scroller = new Scroller(context, null, true);
+ scrollAnimator = ValueAnimator.ofFloat(0, 1);
+ scrollAnimator.addUpdateListener(this);
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java
similarity index 52%
rename from app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java
rename to app/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java
index 74fc6c518..155557ea2 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java
@@ -17,192 +17,183 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.View;
-
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.helpers.UIHelper;
-import org.isoron.uhabits.models.Habit;
-import org.isoron.uhabits.models.Streak;
-
-import java.text.DateFormat;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.TimeZone;
-
-public class HabitStreakView extends View implements HabitDataView
+package org.isoron.uhabits.activities.common.views;
+
+import android.content.*;
+import android.graphics.*;
+import android.util.*;
+import android.view.*;
+import android.view.ViewGroup.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import java.text.*;
+import java.util.*;
+
+import static android.view.View.MeasureSpec.*;
+
+public class StreakChart extends View
{
- private Habit habit;
private Paint paint;
private long minLength;
+
private long maxLength;
private int[] colors;
+
private RectF rect;
+
private int baseSize;
+
private int primaryColor;
+
private List streaks;
private boolean isBackgroundTransparent;
+
private DateFormat dateFormat;
+
private int width;
+
private float em;
+
private float maxLabelWidth;
+
private float textMargin;
+
private boolean shouldShowLabels;
- private int maxStreakCount;
+
private int textColor;
+
private int reverseTextColor;
- public HabitStreakView(Context context)
+ public StreakChart(Context context)
{
super(context);
init();
}
- public HabitStreakView(Context context, AttributeSet attrs)
+ public StreakChart(Context context, AttributeSet attrs)
{
super(context, attrs);
- this.primaryColor = ColorHelper.getColor(getContext(), 7);
init();
}
- public void setHabit(Habit habit)
- {
- this.habit = habit;
- createColors();
- }
-
- private void init()
+ /**
+ * Returns the maximum number of streaks this view is able to show, given
+ * its current size.
+ *
+ * @return max number of visible streaks
+ */
+ public int getMaxStreakCount()
{
- createPaints();
- createColors();
-
- streaks = Collections.emptyList();
-
- dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
- dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
- rect = new RectF();
- maxStreakCount = 10;
- baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
+ return (int) Math.floor(getMeasuredHeight() / baseSize);
}
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+ public void populateWithRandomData()
{
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- setMeasuredDimension(width, height);
- }
+ long day = DateUtils.millisecondsInOneDay;
+ long start = DateUtils.getStartOfToday();
+ LinkedList streaks = new LinkedList<>();
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
- {
- maxStreakCount = height / baseSize;
- this.width = width;
-
- float minTextSize = getResources().getDimension(R.dimen.tinyTextSize);
- float maxTextSize = getResources().getDimension(R.dimen.regularTextSize);
- float textSize = baseSize * 0.5f;
-
- paint.setTextSize(Math.max(Math.min(textSize, maxTextSize), minTextSize));
- em = paint.getFontSpacing();
- textMargin = 0.5f * em;
+ for (int i = 0; i < 10; i++)
+ {
+ int length = new Random().nextInt(100);
+ long end = start + length * day;
+ streaks.add(new Streak(start, end));
+ start = end + day;
+ }
- updateMaxMin();
+ setStreaks(streaks);
}
- private void createColors()
+ public void setColor(int color)
{
- if(habit != null)
- this.primaryColor = ColorHelper.getColor(getContext(), habit.color);
-
- int red = Color.red(primaryColor);
- int green = Color.green(primaryColor);
- int blue = Color.blue(primaryColor);
-
- colors = new int[4];
- colors[3] = primaryColor;
- colors[2] = Color.argb(192, red, green, blue);
- colors[1] = Color.argb(96, red, green, blue);
- colors[0] = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor);
- textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor);
- reverseTextColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastReverseTextColor);
+ this.primaryColor = color;
+ postInvalidate();
}
- protected void createPaints()
+ public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
- paint = new Paint();
- paint.setTextAlign(Paint.Align.CENTER);
- paint.setAntiAlias(true);
+ this.isBackgroundTransparent = isBackgroundTransparent;
+ initColors();
}
- public void refreshData()
+ public void setStreaks(List streaks)
{
- if(habit == null) return;
- streaks = habit.streaks.getAll(maxStreakCount);
- updateMaxMin();
- postInvalidate();
+ this.streaks = streaks;
+ initColors();
+ updateMaxMinLengths();
+ requestLayout();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
- if(streaks.size() == 0) return;
+ if (streaks.size() == 0) return;
rect.set(0, 0, width, baseSize);
- for(Streak s : streaks)
+ for (Streak s : streaks)
{
drawRow(canvas, s, rect);
rect.offset(0, baseSize);
}
}
- private void updateMaxMin()
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec)
{
- maxLength = 0;
- minLength = Long.MAX_VALUE;
- shouldShowLabels = true;
+ LayoutParams params = getLayoutParams();
- for (Streak s : streaks)
+ if (params != null && params.height == LayoutParams.WRAP_CONTENT)
{
- maxLength = Math.max(maxLength, s.length);
- minLength = Math.min(minLength, s.length);
+ int width = getSize(widthSpec);
+ int height = streaks.size() * baseSize;
- float lw1 = paint.measureText(dateFormat.format(new Date(s.start)));
- float lw2 = paint.measureText(dateFormat.format(new Date(s.end)));
- maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
+ heightSpec = makeMeasureSpec(height, EXACTLY);
+ widthSpec = makeMeasureSpec(width, EXACTLY);
}
- if(width - 2 * maxLabelWidth < width * 0.25f)
- {
- maxLabelWidth = 0;
- shouldShowLabels = false;
- }
+ setMeasuredDimension(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void onSizeChanged(int width,
+ int height,
+ int oldWidth,
+ int oldHeight)
+ {
+ this.width = width;
+
+ float minTextSize = getResources().getDimension(R.dimen.tinyTextSize);
+ float maxTextSize =
+ getResources().getDimension(R.dimen.regularTextSize);
+ float textSize = baseSize * 0.5f;
+
+ paint.setTextSize(
+ Math.max(Math.min(textSize, maxTextSize), minTextSize));
+ em = paint.getFontSpacing();
+ textMargin = 0.5f * em;
+
+ updateMaxMinLengths();
}
private void drawRow(Canvas canvas, Streak streak, RectF rect)
{
- if(maxLength == 0) return;
+ if (maxLength == 0) return;
- float percentage = (float) streak.length / maxLength;
+ float percentage = (float) streak.getLength() / maxLength;
float availableWidth = width - 2 * maxLabelWidth;
- if(shouldShowLabels) availableWidth -= 2 * textMargin;
+ if (shouldShowLabels) availableWidth -= 2 * textMargin;
float barWidth = percentage * availableWidth;
- float minBarWidth = paint.measureText(streak.length.toString()) + em;
+ float minBarWidth =
+ paint.measureText(Long.toString(streak.getLength())) + em;
barWidth = Math.max(barWidth, minBarWidth);
float gap = (width - barWidth) / 2;
@@ -210,19 +201,20 @@ public class HabitStreakView extends View implements HabitDataView
paint.setColor(percentageToColor(percentage));
- canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap,
- rect.bottom - paddingTopBottom, paint);
+ canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom,
+ rect.right - gap, rect.bottom - paddingTopBottom, paint);
float yOffset = rect.centerY() + 0.3f * em;
paint.setColor(reverseTextColor);
paint.setTextAlign(Paint.Align.CENTER);
- canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint);
+ canvas.drawText(Long.toString(streak.getLength()), rect.centerX(),
+ yOffset, paint);
- if(shouldShowLabels)
+ if (shouldShowLabels)
{
- String startLabel = dateFormat.format(new Date(streak.start));
- String endLabel = dateFormat.format(new Date(streak.end));
+ String startLabel = dateFormat.format(new Date(streak.getStart()));
+ String endLabel = dateFormat.format(new Date(streak.getEnd()));
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.RIGHT);
@@ -233,17 +225,73 @@ public class HabitStreakView extends View implements HabitDataView
}
}
+ private void init()
+ {
+ initPaints();
+ initColors();
+
+ streaks = Collections.emptyList();
+
+ dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ rect = new RectF();
+ baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
+ }
+
+ private void initColors()
+ {
+ int red = Color.red(primaryColor);
+ int green = Color.green(primaryColor);
+ int blue = Color.blue(primaryColor);
+
+ StyledResources res = new StyledResources(getContext());
+
+ colors = new int[4];
+ colors[3] = primaryColor;
+ colors[2] = Color.argb(192, red, green, blue);
+ colors[1] = Color.argb(96, red, green, blue);
+ colors[0] = res.getColor(R.attr.lowContrastTextColor);
+ textColor = res.getColor(R.attr.mediumContrastTextColor);
+ reverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
+ }
+
+ private void initPaints()
+ {
+ paint = new Paint();
+ paint.setTextAlign(Paint.Align.CENTER);
+ paint.setAntiAlias(true);
+ }
+
private int percentageToColor(float percentage)
{
- if(percentage >= 1.0f) return colors[3];
- if(percentage >= 0.8f) return colors[2];
- if(percentage >= 0.5f) return colors[1];
+ if (percentage >= 1.0f) return colors[3];
+ if (percentage >= 0.8f) return colors[2];
+ if (percentage >= 0.5f) return colors[1];
return colors[0];
}
- public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ private void updateMaxMinLengths()
{
- this.isBackgroundTransparent = isBackgroundTransparent;
- createColors();
+ maxLength = 0;
+ minLength = Long.MAX_VALUE;
+ shouldShowLabels = true;
+
+ for (Streak s : streaks)
+ {
+ maxLength = Math.max(maxLength, s.getLength());
+ minLength = Math.min(minLength, s.getLength());
+
+ float lw1 =
+ paint.measureText(dateFormat.format(new Date(s.getStart())));
+ float lw2 =
+ paint.measureText(dateFormat.format(new Date(s.getEnd())));
+ maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
+ }
+
+ if (width - 2 * maxLabelWidth < width * 0.25f)
+ {
+ maxLabelWidth = 0;
+ shouldShowLabels = false;
+ }
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/package-info.java
new file mode 100644
index 000000000..1d50f26f0
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides views that are used across the app, such as RingView.
+ */
+package org.isoron.uhabits.activities.common.views;
\ No newline at end of file
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/BaseDialog.java
new file mode 100644
index 000000000..70c0e445f
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java
@@ -0,0 +1,262 @@
+/*
+ * 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.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 BaseDialog 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 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_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;
+ }
+
+ @OnItemSelected(R.id.sFrequency)
+ public void onFrequencySelected(int position)
+ {
+ if (position < 0 || position > 4) throw new IllegalArgumentException();
+ int freqNums[] = { 1, 1, 2, 5, 3 };
+ int freqDens[] = { 1, 7, 7, 7, 7 };
+ modifiedHabit.setFrequency(
+ new Frequency(freqNums[position], freqDens[position]));
+ helper.populateFrequencyFields(modifiedHabit);
+ }
+
+ @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/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java
new file mode 100644
index 000000000..78e69c6d4
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java
@@ -0,0 +1,195 @@
+/*
+ * 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.annotation.*;
+import android.support.v4.app.*;
+import android.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import butterknife.*;
+
+public class BaseDialogHelper
+{
+ private DialogFragment frag;
+
+ @BindView(R.id.tvName)
+ TextView tvName;
+
+ @BindView(R.id.tvDescription)
+ TextView tvDescription;
+
+ @BindView(R.id.tvFreqNum)
+ TextView tvFreqNum;
+
+ @BindView(R.id.tvFreqDen)
+ TextView tvFreqDen;
+
+ @BindView(R.id.tvReminderTime)
+ TextView tvReminderTime;
+
+ @BindView(R.id.tvReminderDays)
+ TextView tvReminderDays;
+
+ @BindView(R.id.sFrequency)
+ Spinner sFrequency;
+
+ @BindView(R.id.llCustomFrequency)
+ ViewGroup llCustomFrequency;
+
+ @BindView(R.id.llReminderDays)
+ ViewGroup llReminderDays;
+
+ public BaseDialogHelper(DialogFragment frag, View view)
+ {
+ this.frag = frag;
+ ButterKnife.bind(this, view);
+ }
+
+ protected void populateForm(final Habit habit)
+ {
+ if (habit.getName() != null) tvName.setText(habit.getName());
+ if (habit.getDescription() != null)
+ tvDescription.setText(habit.getDescription());
+
+ populateColor(habit.getColor());
+ populateFrequencyFields(habit);
+ populateReminderFields(habit);
+ }
+
+ void parseFormIntoHabit(Habit habit)
+ {
+ 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())
+ {
+ int numerator = Integer.parseInt(freqNum);
+ int denominator = Integer.parseInt(freqDen);
+ habit.setFrequency(new Frequency(numerator, denominator));
+ }
+ }
+
+ void populateColor(int paletteColor)
+ {
+ tvName.setTextColor(
+ ColorUtils.getColor(frag.getContext(), paletteColor));
+ }
+
+ @SuppressLint("SetTextI18n")
+ void populateFrequencyFields(Habit habit)
+ {
+ int quickSelectPosition = -1;
+
+ Frequency freq = habit.getFrequency();
+
+ if (freq.equals(Frequency.DAILY))
+ quickSelectPosition = 0;
+
+ else if (freq.equals(Frequency.WEEKLY))
+ quickSelectPosition = 1;
+
+ else if (freq.equals(Frequency.TWO_TIMES_PER_WEEK))
+ quickSelectPosition = 2;
+
+ else if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK))
+ quickSelectPosition = 3;
+
+ if (quickSelectPosition >= 0)
+ showSimplifiedFrequency(quickSelectPosition);
+
+ else showCustomFrequency();
+
+ tvFreqNum.setText(Integer.toString(freq.getNumerator()));
+ tvFreqDen.setText(Integer.toString(freq.getDenominator()));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ void populateReminderFields(Habit habit)
+ {
+ if (!habit.hasReminder())
+ {
+ tvReminderTime.setText(R.string.reminder_off);
+ llReminderDays.setVisibility(View.GONE);
+ return;
+ }
+
+ Reminder reminder = habit.getReminder();
+
+ String time =
+ DateUtils.formatTime(frag.getContext(), reminder.getHour(),
+ reminder.getMinute());
+ tvReminderTime.setText(time);
+ llReminderDays.setVisibility(View.VISIBLE);
+
+ boolean weekdays[] = reminder.getDays().toArray();
+ tvReminderDays.setText(
+ DateUtils.formatWeekdayList(frag.getContext(), weekdays));
+ }
+
+ private void showCustomFrequency()
+ {
+ sFrequency.setVisibility(View.GONE);
+ llCustomFrequency.setVisibility(View.VISIBLE);
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void showSimplifiedFrequency(int quickSelectPosition)
+ {
+ sFrequency.setVisibility(View.VISIBLE);
+ sFrequency.setSelection(quickSelectPosition);
+ llCustomFrequency.setVisibility(View.GONE);
+ }
+
+ boolean validate(Habit habit)
+ {
+ Boolean valid = true;
+
+ if (habit.getName().length() == 0)
+ {
+ tvName.setError(
+ frag.getString(R.string.validation_name_should_not_be_blank));
+ valid = false;
+ }
+
+ Frequency freq = habit.getFrequency();
+
+ 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/CreateHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java
new file mode 100644
index 000000000..5f34e2e40
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java
@@ -0,0 +1,54 @@
+/*
+ * 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 CreateHabitDialog extends BaseDialog
+{
+ @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()));
+ }
+
+ @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/EditHabitDialog.java
new file mode 100644
index 000000000..e9c1aca78
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java
@@ -0,0 +1,52 @@
+/*
+ * 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 org.isoron.uhabits.*;
+import org.isoron.uhabits.commands.*;
+
+public class EditHabitDialog extends BaseDialog
+{
+ @Override
+ protected int getTitle()
+ {
+ return R.string.edit_habit;
+ }
+
+ @Override
+ protected void initializeHabits()
+ {
+ Long habitId = (Long) getArguments().get("habitId");
+ if (habitId == null)
+ throw new IllegalArgumentException("habitId must be specified");
+
+ originalHabit = habitList.getById(habitId);
+ modifiedHabit = modelFactory.buildHabit();
+ modifiedHabit.copyFrom(originalHabit);
+ }
+
+ @Override
+ protected void saveHabit()
+ {
+ Command command = appComponent.getEditHabitCommandFactory().
+ create(habitList, originalHabit, modifiedHabit);
+ commandRunner.execute(command, originalHabit.getId());
+ }
+}
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/EditHabitDialogFactory.java
new file mode 100644
index 000000000..481658ebf
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.isoron.uhabits.models.*;
+
+import javax.inject.*;
+
+public class EditHabitDialogFactory
+{
+ @Inject
+ public EditHabitDialogFactory()
+ {
+ }
+
+ public EditHabitDialog create(@NonNull Habit habit)
+ {
+ if (habit.getId() == null)
+ throw new IllegalArgumentException("habit not saved");
+
+ EditHabitDialog dialog = new EditHabitDialog();
+ Bundle args = new Bundle();
+ args.putLong("habitId", habit.getId());
+ dialog.setArguments(args);
+ return dialog;
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/package-info.java
new file mode 100644
index 000000000..3d6e4e626
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides dialogs for editing habits and related classes.
+ */
+package org.isoron.uhabits.activities.habits.edit;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java
new file mode 100644
index 000000000..d5529c2a1
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+import android.os.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+import org.isoron.uhabits.preferences.*;
+
+/**
+ * Activity that allows the user to see and modify the list of habits.
+ */
+public class ListHabitsActivity extends BaseActivity
+{
+ private HabitCardListAdapter adapter;
+
+ private ListHabitsRootView rootView;
+
+ private ListHabitsScreen screen;
+
+ private ListHabitsComponent component;
+
+ private boolean pureBlack;
+
+ private Preferences prefs;
+
+ public ListHabitsComponent getListHabitsComponent()
+ {
+ return component;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ HabitsApplication app = (HabitsApplication) getApplicationContext();
+
+ component = DaggerListHabitsComponent
+ .builder()
+ .appComponent(app.getComponent())
+ .activityModule(new ActivityModule(this))
+ .build();
+
+ ListHabitsMenu menu = component.getMenu();
+ ListHabitsSelectionMenu selectionMenu = component.getSelectionMenu();
+ ListHabitsController controller = component.getController();
+
+ adapter = component.getAdapter();
+ rootView = component.getRootView();
+ screen = component.getScreen();
+
+ prefs = app.getComponent().getPreferences();
+ pureBlack = prefs.isPureBlackEnabled();
+
+ screen.setMenu(menu);
+ screen.setController(controller);
+ screen.setSelectionMenu(selectionMenu);
+ rootView.setController(controller, selectionMenu);
+
+ setScreen(screen);
+ controller.onStartup();
+ }
+
+ @Override
+ protected void onPause()
+ {
+ screen.onDettached();
+ adapter.cancelRefresh();
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume()
+ {
+ adapter.refresh();
+ screen.onAttached();
+ rootView.postInvalidate();
+
+ if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
+ prefs.isPureBlackEnabled() != pureBlack)
+ {
+ restartWithFade();
+ }
+
+ super.onResume();
+ }
+}
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
new file mode 100644
index 000000000..658d6c573
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+
+import dagger.*;
+
+@ActivityScope
+@Component(modules = { ActivityModule.class },
+ dependencies = { AppComponent.class })
+public interface ListHabitsComponent extends ActivityComponent
+{
+ CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory();
+
+ HabitCardListAdapter getAdapter();
+
+ ListHabitsController getController();
+
+ ListHabitsMenu getMenu();
+
+ ListHabitsRootView getRootView();
+
+ ListHabitsScreen getScreen();
+
+ ListHabitsSelectionMenu getSelectionMenu();
+}
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
new file mode 100644
index 000000000..af89c14ff
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+import org.isoron.uhabits.commands.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.preferences.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+import org.isoron.uhabits.widgets.*;
+
+import java.io.*;
+import java.util.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ListHabitsController
+ implements HabitCardListController.HabitListener
+{
+ @NonNull
+ private final ListHabitsScreen screen;
+
+ @NonNull
+ private final BaseSystem system;
+
+ @NonNull
+ private final HabitList habitList;
+
+ @NonNull
+ private final HabitCardListAdapter adapter;
+
+ @NonNull
+ private final Preferences prefs;
+
+ @NonNull
+ private final CommandRunner commandRunner;
+
+ @NonNull
+ private final TaskRunner taskRunner;
+
+ private ReminderScheduler reminderScheduler;
+
+ private WidgetUpdater widgetUpdater;
+
+ private ImportDataTaskFactory importTaskFactory;
+
+ private ExportCSVTaskFactory exportCSVFactory;
+
+ @Inject
+ public ListHabitsController(@NonNull BaseSystem system,
+ @NonNull CommandRunner commandRunner,
+ @NonNull HabitList habitList,
+ @NonNull HabitCardListAdapter adapter,
+ @NonNull ListHabitsScreen screen,
+ @NonNull Preferences prefs,
+ @NonNull ReminderScheduler reminderScheduler,
+ @NonNull TaskRunner taskRunner,
+ @NonNull WidgetUpdater widgetUpdater,
+ @NonNull
+ ImportDataTaskFactory importTaskFactory,
+ @NonNull ExportCSVTaskFactory exportCSVFactory)
+ {
+ this.adapter = adapter;
+ this.commandRunner = commandRunner;
+ this.habitList = habitList;
+ this.prefs = prefs;
+ this.screen = screen;
+ this.system = system;
+ this.taskRunner = taskRunner;
+ this.reminderScheduler = reminderScheduler;
+ this.widgetUpdater = widgetUpdater;
+ this.importTaskFactory = importTaskFactory;
+ this.exportCSVFactory = exportCSVFactory;
+ }
+
+ public void onExportCSV()
+ {
+ List selected = new LinkedList<>();
+ for (Habit h : habitList) selected.add(h);
+
+ taskRunner.execute(exportCSVFactory.create(selected, filename -> {
+ if (filename != null) screen.showSendFileScreen(filename);
+ else screen.showMessage(R.string.could_not_export);
+ }));
+ }
+
+ public void onExportDB()
+ {
+ taskRunner.execute(new ExportDBTask(filename -> {
+ if (filename != null) screen.showSendFileScreen(filename);
+ else screen.showMessage(R.string.could_not_export);
+ }));
+ }
+
+ @Override
+ public void onHabitClick(@NonNull Habit h)
+ {
+ screen.showHabitScreen(h);
+ }
+
+ @Override
+ public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
+ {
+ taskRunner.execute(() -> habitList.reorder(from, to));
+ }
+
+ public void onImportData(@NonNull File file)
+ {
+ taskRunner.execute(importTaskFactory.create(file, result -> {
+ switch (result)
+ {
+ case ImportDataTask.SUCCESS:
+ adapter.refresh();
+ screen.showMessage(R.string.habits_imported);
+ break;
+
+ case ImportDataTask.NOT_RECOGNIZED:
+ screen.showMessage(R.string.file_not_recognized);
+ break;
+
+ default:
+ screen.showMessage(R.string.could_not_import);
+ break;
+ }
+ }));
+ }
+
+
+ @Override
+ public void onInvalidToggle()
+ {
+ screen.showMessage(R.string.long_press_to_toggle);
+ }
+
+ public void onRepairDB()
+ {
+ taskRunner.execute(() -> {
+ habitList.repair();
+ screen.showMessage(R.string.database_repaired);
+ });
+ }
+
+ public void onSendBugReport()
+ {
+ try
+ {
+ system.dumpBugReportToFile();
+ }
+ catch (IOException e)
+ {
+ // ignored
+ }
+
+ try
+ {
+ String log = system.getBugReport();
+ int to = R.string.bugReportTo;
+ int subject = R.string.bugReportSubject;
+ screen.showSendEmailScreen(to, subject, log);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ screen.showMessage(R.string.bug_report_failed);
+ }
+ }
+
+ public void onStartup()
+ {
+ prefs.incrementLaunchCount();
+ if (prefs.isFirstRun()) onFirstRun();
+ }
+
+ @Override
+ public void onToggle(@NonNull Habit habit, long timestamp)
+ {
+ commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp),
+ habit.getId());
+ }
+
+ private void onFirstRun()
+ {
+ prefs.setFirstRun(false);
+ prefs.updateLastHint(-1, DateUtils.getStartOfToday());
+ screen.showIntroScreen();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java
new file mode 100644
index 000000000..dc8fe057d
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+import org.isoron.uhabits.preferences.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ListHabitsMenu extends BaseMenu
+{
+ @NonNull
+ private final ListHabitsScreen screen;
+
+ private final HabitCardListAdapter adapter;
+
+ private boolean showArchived;
+
+ private boolean showCompleted;
+
+ private final Preferences preferences;
+
+ private ThemeSwitcher themeSwitcher;
+
+ @Inject
+ public ListHabitsMenu(@NonNull BaseActivity activity,
+ @NonNull ListHabitsScreen screen,
+ @NonNull HabitCardListAdapter adapter,
+ @NonNull Preferences preferences,
+ @NonNull ThemeSwitcher themeSwitcher)
+ {
+ super(activity);
+ this.screen = screen;
+ this.adapter = adapter;
+ this.preferences = preferences;
+ this.themeSwitcher = themeSwitcher;
+
+ showCompleted = preferences.getShowCompleted();
+ showArchived = preferences.getShowArchived();
+ updateAdapterFilter();
+ }
+
+ @Override
+ public void onCreate(@NonNull Menu menu)
+ {
+ MenuItem nightModeItem = menu.findItem(R.id.actionToggleNightMode);
+ nightModeItem.setChecked(themeSwitcher.isNightMode());
+
+ MenuItem hideArchivedItem = menu.findItem(R.id.actionHideArchived);
+ hideArchivedItem.setChecked(!showArchived);
+
+ MenuItem hideCompletedItem = menu.findItem(R.id.actionHideCompleted);
+ hideCompletedItem.setChecked(!showCompleted);
+ }
+
+ @Override
+ public boolean onItemSelected(@NonNull MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.actionToggleNightMode:
+ screen.toggleNightMode();
+ return true;
+
+ case R.id.actionAdd:
+ screen.showCreateHabitScreen();
+ return true;
+
+ case R.id.actionFAQ:
+ screen.showFAQScreen();
+ return true;
+
+ case R.id.actionAbout:
+ screen.showAboutScreen();
+ return true;
+
+ case R.id.actionSettings:
+ screen.showSettingsScreen();
+ return true;
+
+ case R.id.actionHideArchived:
+ toggleShowArchived();
+ invalidate();
+ return true;
+
+ case R.id.actionHideCompleted:
+ toggleShowCompleted();
+ invalidate();
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected int getMenuResourceId()
+ {
+ return R.menu.list_habits;
+ }
+
+ private void toggleShowArchived()
+ {
+ showArchived = !showArchived;
+ preferences.setShowArchived(showArchived);
+ updateAdapterFilter();
+ }
+
+ private void toggleShowCompleted()
+ {
+ showCompleted = !showCompleted;
+ preferences.setShowCompleted(showCompleted);
+ updateAdapterFilter();
+ }
+
+ private void updateAdapterFilter()
+ {
+ adapter.setFilter(new HabitMatcherBuilder()
+ .setArchivedAllowed(showArchived)
+ .setCompletedAllowed(showCompleted)
+ .build());
+ adapter.refresh();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java
new file mode 100644
index 000000000..7d1bb3568
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java
@@ -0,0 +1,187 @@
+/*
+ * 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;
+
+import android.content.*;
+import android.content.res.*;
+import android.support.annotation.*;
+import android.support.v7.widget.Toolbar;
+import android.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+import org.isoron.uhabits.activities.habits.list.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import javax.inject.*;
+
+import butterknife.*;
+
+@ActivityScope
+public class ListHabitsRootView extends BaseRootView
+ implements ModelObservable.Listener, TaskRunner.Listener
+{
+ public static final int MAX_CHECKMARK_COUNT = 21;
+
+ @BindView(R.id.listView)
+ HabitCardListView listView;
+
+ @BindView(R.id.llEmpty)
+ ViewGroup llEmpty;
+
+ @BindView(R.id.tvStarEmpty)
+ TextView tvStarEmpty;
+
+ @BindView(R.id.toolbar)
+ Toolbar toolbar;
+
+ @BindView(R.id.progressBar)
+ ProgressBar progressBar;
+
+ @BindView(R.id.hintView)
+ HintView hintView;
+
+ @BindView(R.id.header)
+ HeaderView header;
+
+ @NonNull
+ private final HabitCardListAdapter listAdapter;
+
+ private final TaskRunner runner;
+
+ @Inject
+ public ListHabitsRootView(@ActivityContext Context context,
+ @NonNull HintListFactory hintListFactory,
+ @NonNull HabitCardListAdapter listAdapter,
+ @NonNull TaskRunner runner)
+ {
+ super(context);
+ addView(inflate(getContext(), R.layout.list_habits, null));
+ ButterKnife.bind(this);
+
+ this.listAdapter = listAdapter;
+ listView.setAdapter(listAdapter);
+ listAdapter.setListView(listView);
+
+ this.runner = runner;
+ progressBar.setIndeterminate(true);
+ tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
+
+ String hints[] =
+ getContext().getResources().getStringArray(R.array.hints);
+ HintList hintList = hintListFactory.create(hints);
+ hintView.setHints(hintList);
+
+ initToolbar();
+ }
+
+ @NonNull
+ @Override
+ public Toolbar getToolbar()
+ {
+ return toolbar;
+ }
+
+ @Override
+ public void onModelChange()
+ {
+ updateEmptyView();
+ }
+
+ @Override
+ public void onTaskFinished(Task task)
+ {
+ updateProgressBar();
+ }
+
+ @Override
+ public void onTaskStarted(Task task)
+ {
+ updateProgressBar();
+ }
+
+ public void setController(@NonNull ListHabitsController controller,
+ @NonNull ListHabitsSelectionMenu menu)
+ {
+ HabitCardListController listController =
+ new HabitCardListController(listAdapter);
+
+ listController.setHabitListener(controller);
+ listController.setSelectionListener(menu);
+ listView.setController(listController);
+ menu.setListController(listController);
+ }
+
+ @Override
+ protected void onAttachedToWindow()
+ {
+ super.onAttachedToWindow();
+ runner.addListener(this);
+ updateProgressBar();
+ listAdapter.getObservable().addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ listAdapter.getObservable().removeListener(this);
+ runner.removeListener(this);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh)
+ {
+ int count = getCheckmarkCount();
+ header.setButtonCount(count);
+ listView.setCheckmarkCount(count);
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ private int getCheckmarkCount()
+ {
+ Resources res = getResources();
+ float labelWidth = Math.max(getMeasuredWidth() / 3, res.getDimension(R.dimen.habitNameWidth));
+ float buttonWidth = res.getDimension(R.dimen.checkmarkWidth);
+ return Math.min(MAX_CHECKMARK_COUNT, Math.max(0,
+ (int) ((getMeasuredWidth() - labelWidth) / buttonWidth)));
+ }
+
+ private void updateEmptyView()
+ {
+ llEmpty.setVisibility(
+ listAdapter.getItemCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+
+ private void updateProgressBar()
+ {
+ postDelayed(() -> {
+ int activeTaskCount = runner.getActiveTaskCount();
+ int newVisibility = activeTaskCount > 0 ? VISIBLE : GONE;
+ if (progressBar.getVisibility() != newVisibility)
+ progressBar.setVisibility(newVisibility);
+ }, 500);
+ }
+}
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
new file mode 100644
index 000000000..3fb92e817
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java
@@ -0,0 +1,244 @@
+/*
+ * 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;
+
+import android.content.*;
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.common.dialogs.*;
+import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialog.*;
+import org.isoron.uhabits.activities.habits.edit.*;
+import org.isoron.uhabits.commands.*;
+import org.isoron.uhabits.intents.*;
+import org.isoron.uhabits.io.*;
+import org.isoron.uhabits.models.*;
+
+import java.io.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ListHabitsScreen extends BaseScreen
+ implements CommandRunner.Listener
+{
+ 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_REPAIR_DB = 5;
+
+ public static final int RESULT_IMPORT_DATA = 1;
+
+ @Nullable
+ private ListHabitsController controller;
+
+ @NonNull
+ private final IntentFactory intentFactory;
+
+ @NonNull
+ private final DirFinder dirFinder;
+
+ @NonNull
+ private final CommandRunner commandRunner;
+
+ @NonNull
+ private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory;
+
+ @NonNull
+ private final CreateHabitDialogFactory createHabitDialogFactory;
+
+ @NonNull
+ private final FilePickerDialogFactory filePickerDialogFactory;
+
+ @NonNull
+ private final ColorPickerDialogFactory colorPickerFactory;
+
+ @NonNull
+ private final EditHabitDialogFactory editHabitDialogFactory;
+
+ @NonNull
+ private final ThemeSwitcher themeSwitcher;
+
+ @Inject
+ public ListHabitsScreen(@NonNull BaseActivity activity,
+ @NonNull CommandRunner commandRunner,
+ @NonNull DirFinder dirFinder,
+ @NonNull ListHabitsRootView rootView,
+ @NonNull IntentFactory intentFactory,
+ @NonNull ThemeSwitcher themeSwitcher,
+ @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
+ @NonNull CreateHabitDialogFactory createHabitDialogFactory,
+ @NonNull FilePickerDialogFactory filePickerDialogFactory,
+ @NonNull ColorPickerDialogFactory colorPickerFactory,
+ @NonNull EditHabitDialogFactory editHabitDialogFactory)
+ {
+ super(activity);
+ setRootView(rootView);
+ this.editHabitDialogFactory = editHabitDialogFactory;
+ this.colorPickerFactory = colorPickerFactory;
+ this.commandRunner = commandRunner;
+ this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
+ this.createHabitDialogFactory = createHabitDialogFactory;
+ this.dirFinder = dirFinder;
+ this.filePickerDialogFactory = filePickerDialogFactory;
+ this.intentFactory = intentFactory;
+ this.themeSwitcher = themeSwitcher;
+ }
+
+ public void onAttached()
+ {
+ commandRunner.addListener(this);
+ }
+
+ @Override
+ public void onCommandExecuted(@NonNull Command command,
+ @Nullable Long refreshKey)
+ {
+ showMessage(command.getExecuteStringId());
+ }
+
+ public void onDettached()
+ {
+ commandRunner.removeListener(this);
+ }
+
+ @Override
+ public void onResult(int requestCode, int resultCode, Intent data)
+ {
+ 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 void setController(@Nullable ListHabitsController controller)
+ {
+ this.controller = controller;
+ }
+
+ public void showAboutScreen()
+ {
+ Intent intent = intentFactory.startAboutActivity(activity);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Displays a {@link ColorPickerDialog} to the user.
+ *
+ * The selected color on the dialog is the color of the given habit.
+ *
+ * @param habit the habit
+ * @param callback
+ */
+ public void showColorPicker(@NonNull Habit habit,
+ @NonNull OnColorSelectedListener callback)
+ {
+ ColorPickerDialog picker = colorPickerFactory.create(habit.getColor());
+ picker.setListener(callback);
+ activity.showDialog(picker, "picker");
+ }
+
+ public void showCreateHabitScreen()
+ {
+ activity.showDialog(createHabitDialogFactory.create(), "editHabit");
+ }
+
+ public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback)
+ {
+ activity.showDialog(confirmDeleteDialogFactory.create(callback));
+ }
+
+ public void showEditHabitScreen(Habit habit)
+ {
+ EditHabitDialog dialog = editHabitDialogFactory.create(habit);
+ activity.showDialog(dialog, "editHabit");
+ }
+
+ public void showFAQScreen()
+ {
+ Intent intent = intentFactory.viewFAQ(activity);
+ activity.startActivity(intent);
+ }
+
+ public void showHabitScreen(@NonNull Habit habit)
+ {
+ Intent intent = intentFactory.startShowHabitActivity(activity, habit);
+ activity.startActivity(intent);
+ }
+
+ public void showImportScreen()
+ {
+ File dir = dirFinder.findStorageDir(null);
+
+ if (dir == null)
+ {
+ showMessage(R.string.could_not_import);
+ return;
+ }
+
+ FilePickerDialog picker = filePickerDialogFactory.create(dir);
+
+ if (controller != null)
+ picker.setListener(file -> controller.onImportData(file));
+ activity.showDialog(picker.getDialog());
+ }
+
+ public void showIntroScreen()
+ {
+ Intent intent = intentFactory.startIntroActivity(activity);
+ activity.startActivity(intent);
+ }
+
+ public void showSettingsScreen()
+ {
+ Intent intent = intentFactory.startSettingsActivity(activity);
+ activity.startActivityForResult(intent, 0);
+ }
+
+ public void toggleNightMode()
+ {
+ themeSwitcher.toggleNightMode();
+ activity.restartWithFade();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java
new file mode 100644
index 000000000..9543fe352
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+import android.support.annotation.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.commands.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+
+import java.util.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ListHabitsSelectionMenu extends BaseSelectionMenu
+ implements HabitCardListController.SelectionListener
+{
+ @NonNull
+ private final ListHabitsScreen screen;
+
+ @NonNull
+ CommandRunner commandRunner;
+
+ @NonNull
+ private final HabitCardListAdapter listAdapter;
+
+ @Nullable
+ private HabitCardListController listController;
+
+ @NonNull
+ private final HabitList habitList;
+
+ @Inject
+ public ListHabitsSelectionMenu(@NonNull HabitList habitList,
+ @NonNull ListHabitsScreen screen,
+ @NonNull HabitCardListAdapter listAdapter,
+ @NonNull CommandRunner commandRunner)
+ {
+ this.habitList = habitList;
+ this.screen = screen;
+ this.listAdapter = listAdapter;
+ this.commandRunner = commandRunner;
+ }
+
+ @Override
+ public void onFinish()
+ {
+ if (listController != null) listController.onSelectionFinished();
+ super.onFinish();
+ }
+
+ @Override
+ public boolean onItemClicked(@NonNull MenuItem item)
+ {
+ List selected = listAdapter.getSelected();
+ if (selected.isEmpty()) return false;
+
+ Habit firstHabit = selected.get(0);
+
+ switch (item.getItemId())
+ {
+ case R.id.action_edit_habit:
+ showEditScreen(firstHabit);
+ finish();
+ return true;
+
+ case R.id.action_archive_habit:
+ performArchive(selected);
+ finish();
+ return true;
+
+ case R.id.action_unarchive_habit:
+ performUnarchive(selected);
+ finish();
+ return true;
+
+ case R.id.action_delete:
+ performDelete(selected);
+ return true;
+
+ case R.id.action_color:
+ showColorPicker(selected, firstHabit);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onPrepare(@NonNull Menu menu)
+ {
+ List selected = listAdapter.getSelected();
+
+ boolean showEdit = (selected.size() == 1);
+ boolean showArchive = true;
+ boolean showUnarchive = true;
+ for (Habit h : selected)
+ {
+ if (h.isArchived()) showArchive = false;
+ else showUnarchive = false;
+ }
+
+ MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
+ MenuItem itemColor = menu.findItem(R.id.action_color);
+ MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
+ MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
+
+ itemColor.setVisible(true);
+ itemEdit.setVisible(showEdit);
+ itemArchive.setVisible(showArchive);
+ itemUnarchive.setVisible(showUnarchive);
+
+ setTitle(Integer.toString(selected.size()));
+
+ return true;
+ }
+
+ @Override
+ public void onSelectionChange()
+ {
+ invalidate();
+ }
+
+ @Override
+ public void onSelectionFinish()
+ {
+ finish();
+ }
+
+ @Override
+ public void onSelectionStart()
+ {
+ screen.startSelection();
+ }
+
+ public void setListController(HabitCardListController listController)
+ {
+ this.listController = listController;
+ }
+
+ @Override
+ protected int getResourceId()
+ {
+ return R.menu.list_habits_selection;
+ }
+
+ private void performArchive(@NonNull List selected)
+ {
+ commandRunner.execute(new ArchiveHabitsCommand(habitList, selected),
+ null);
+ }
+
+ private void performDelete(@NonNull List selected)
+ {
+ screen.showDeleteConfirmationScreen(() -> {
+ listAdapter.performRemove(selected);
+ commandRunner.execute(new DeleteHabitsCommand(habitList, selected),
+ null);
+ finish();
+ });
+ }
+
+ private void performUnarchive(@NonNull List selected)
+ {
+ commandRunner.execute(new UnarchiveHabitsCommand(habitList, selected),
+ null);
+ }
+
+ private void showColorPicker(@NonNull List selected,
+ @NonNull Habit firstHabit)
+ {
+ screen.showColorPicker(firstHabit, color -> {
+ commandRunner.execute(
+ new ChangeHabitColorCommand(habitList, selected, color), null);
+ finish();
+ });
+ }
+
+ private void showEditScreen(@NonNull Habit firstHabit)
+ {
+ screen.showEditHabitScreen(firstHabit);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.java
new file mode 100644
index 000000000..382e12cda
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.java
@@ -0,0 +1,99 @@
+/*
+ * 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 CheckmarkButtonController
+{
+ @Nullable
+ private CheckmarkButtonView view;
+
+ @Nullable
+ private Listener listener;
+
+ @NonNull
+ private final Preferences prefs;
+
+ @NonNull
+ private Habit habit;
+
+ private long timestamp;
+
+ public CheckmarkButtonController(@Provided @NonNull Preferences prefs,
+ @NonNull Habit habit,
+ long timestamp)
+ {
+ this.habit = habit;
+ this.timestamp = timestamp;
+ this.prefs = prefs;
+ }
+
+ public void onClick()
+ {
+ if (prefs.isShortToggleEnabled()) performToggle();
+ else performInvalidToggle();
+ }
+
+ public boolean onLongClick()
+ {
+ performToggle();
+ return true;
+ }
+
+ public void performInvalidToggle()
+ {
+ if (listener != null) listener.onInvalidToggle();
+ }
+
+ public void performToggle()
+ {
+ if (view != null) view.toggle();
+ if (listener != null) listener.onToggle(habit, timestamp);
+ }
+
+ public void setListener(@Nullable Listener listener)
+ {
+ this.listener = listener;
+ }
+
+ public void setView(@Nullable CheckmarkButtonView view)
+ {
+ this.view = view;
+ }
+
+ public interface Listener
+ {
+ /**
+ * Called when the user's attempt to perform a toggle is rejected.
+ */
+ void onInvalidToggle();
+
+
+ void onToggle(@NonNull Habit habit, long timestamp);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java
similarity index 54%
rename from app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java
rename to app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java
index 3b46ec95c..01e2b4643 100644
--- a/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java
@@ -17,41 +17,45 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.tasks;
+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;
-public class ToggleRepetitionTask extends BaseTask
-{;
- public interface Listener {
- void onToggleRepetitionFinished();
- }
+public class HabitCardController implements HabitCardView.Controller
+{
+ @Nullable
+ private HabitCardView view;
+ @Nullable
private Listener listener;
- private final Habit habit;
- private final Long timestamp;
- public ToggleRepetitionTask(Habit habit, Long timestamp)
+ @Override
+ public void onInvalidToggle()
{
- this.timestamp = timestamp;
- this.habit = habit;
+ if (listener != null) listener.onInvalidToggle();
}
@Override
- protected void doInBackground()
+ public void onToggle(@NonNull Habit habit, long timestamp)
{
- habit.repetitions.toggle(timestamp);
+ if (view != null) view.triggerRipple(timestamp);
+ if (listener != null) listener.onToggle(habit, timestamp);
}
- @Override
- protected void onPostExecute(Void aVoid)
+ public void setListener(@Nullable Listener listener)
{
- if(listener != null) listener.onToggleRepetitionFinished();
- super.onPostExecute(null);
+ this.listener = listener;
}
- public void setListener(Listener listener)
+ public void setView(@Nullable HabitCardView view)
+ {
+ this.view = view;
+ }
+
+ public interface Listener extends CheckmarkButtonController.Listener
{
- this.listener = listener;
}
-}
\ No newline at end of file
+}
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
new file mode 100644
index 000000000..d710c3572
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java
@@ -0,0 +1,300 @@
+/*
+ * 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 org.isoron.uhabits.models.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+import org.isoron.uhabits.activities.habits.list.views.*;
+
+/**
+ * Controller responsible for receiving and processing the events generated by a
+ * HabitListView. These include selecting and reordering items, toggling
+ * checkmarks and clicking habits.
+ */
+public class HabitCardListController implements HabitCardListView.Controller
+{
+ private final Mode NORMAL_MODE = new NormalMode();
+
+ private final Mode SELECTION_MODE = new SelectionMode();
+
+ @NonNull
+ private final HabitCardListAdapter adapter;
+
+ @Nullable
+ private HabitListener habitListener;
+
+ @Nullable
+ private SelectionListener selectionListener;
+
+ @NonNull
+ private Mode activeMode;
+
+ public HabitCardListController(@NonNull HabitCardListAdapter adapter)
+ {
+ this.adapter = adapter;
+ this.activeMode = new NormalMode();
+ }
+
+ /**
+ * Called when the user drags a habit and drops it somewhere. Note that the
+ * dragging operation is already complete.
+ *
+ * @param from the original position of the habit
+ * @param to the position where the habit was released
+ */
+ @Override
+ public void drop(int from, int to)
+ {
+ if (from == to) return;
+ cancelSelection();
+
+ Habit habitFrom = adapter.getItem(from);
+ Habit habitTo = adapter.getItem(to);
+ adapter.performReorder(from, to);
+
+ if (habitListener != null)
+ habitListener.onHabitReorder(habitFrom, habitTo);
+ }
+
+ /**
+ * Called when the user attempts to perform a toggle, but attempt is
+ * rejected.
+ */
+ @Override
+ public void onInvalidToggle()
+ {
+ if (habitListener != null) habitListener.onInvalidToggle();
+ }
+
+ /**
+ * Called when the user clicks at some item.
+ *
+ * @param position the position of the clicked item
+ */
+ @Override
+ public void onItemClick(int position)
+ {
+ activeMode.onItemClick(position);
+ }
+
+ /**
+ * Called when the user long clicks at some item.
+ *
+ * @param position the position of the clicked item
+ */
+ @Override
+ public void onItemLongClick(int position)
+ {
+ activeMode.onItemLongClick(position);
+ }
+
+ /**
+ * Called when the selection operation is cancelled externally, by something
+ * other than this controller. This happens, for example, when the user
+ * presses the back button.
+ */
+ public void onSelectionFinished()
+ {
+ cancelSelection();
+ }
+
+ /**
+ * Called when the user wants to toggle a checkmark.
+ *
+ * @param habit the habit of the checkmark
+ * @param timestamp the timestamps of the checkmark
+ */
+ @Override
+ public void onToggle(@NonNull Habit habit, long timestamp)
+ {
+ if (habitListener != null) habitListener.onToggle(habit, timestamp);
+ }
+
+ public void setHabitListener(@Nullable HabitListener habitListener)
+ {
+ this.habitListener = habitListener;
+ }
+
+ public void setSelectionListener(@Nullable SelectionListener listener)
+ {
+ this.selectionListener = listener;
+ }
+
+ /**
+ * Called when the user starts dragging an item.
+ *
+ * @param position the position of the habit dragged
+ */
+ @Override
+ public void startDrag(int position)
+ {
+ activeMode.startDrag(position);
+ }
+
+ /**
+ * Selects or deselects the item at a given position
+ *
+ * @param position the position of the item to be selected/deselected
+ */
+ protected void toggleSelection(int position)
+ {
+ adapter.toggleSelection(position);
+ activeMode = adapter.isSelectionEmpty() ? NORMAL_MODE : SELECTION_MODE;
+ }
+
+ /**
+ * Marks all items as not selected and finishes the selection operation.
+ */
+ private void cancelSelection()
+ {
+ adapter.clearSelection();
+ activeMode = new NormalMode();
+
+ if (selectionListener != null) selectionListener.onSelectionFinish();
+ }
+
+ public interface HabitListener extends CheckmarkButtonController.Listener
+ {
+ /**
+ * Called when the user clicks a habit.
+ *
+ * @param habit the habit clicked
+ */
+ void onHabitClick(@NonNull Habit habit);
+
+ /**
+ * Called when the user wants to change the position of a habit on the
+ * list.
+ *
+ * @param from habit to be moved
+ * @param to habit that currently occupies the desired position
+ */
+ void onHabitReorder(@NonNull Habit from, @NonNull Habit to);
+ }
+
+ /**
+ * A Mode describes the behaviour of the list upon clicking, long clicking
+ * and dragging an item. This depends on whether some items are already
+ * selected or not.
+ */
+ private interface Mode
+ {
+ void onItemClick(int position);
+
+ boolean onItemLongClick(int position);
+
+ void startDrag(int position);
+ }
+
+ public interface SelectionListener
+ {
+ /**
+ * Called when the user changes the list of selected item. This is only
+ * called if there were previously selected items. If the selection was
+ * previously empty, then onHabitSelectionStart is called instead.
+ */
+ void onSelectionChange();
+
+ /**
+ * Called when the user deselects all items or cancels the selection.
+ */
+ void onSelectionFinish();
+
+ /**
+ * Called after the user selects the first item.
+ */
+ void onSelectionStart();
+ }
+
+ /**
+ * Mode activated when there are no items selected. Clicks trigger habit
+ * click. Long clicks start selection.
+ */
+ class NormalMode implements Mode
+ {
+ @Override
+ public void onItemClick(int position)
+ {
+ Habit habit = adapter.getItem(position);
+ if (habitListener != null) habitListener.onHabitClick(habit);
+ }
+
+ @Override
+ public boolean onItemLongClick(int position)
+ {
+ startSelection(position);
+ return true;
+ }
+
+ @Override
+ public void startDrag(int position)
+ {
+ startSelection(position);
+ }
+
+ protected void startSelection(int position)
+ {
+ toggleSelection(position);
+ activeMode = SELECTION_MODE;
+ if (selectionListener != null) selectionListener.onSelectionStart();
+ }
+ }
+
+ /**
+ * Mode activated when some items are already selected.
+ *
+ * Clicks toggle item selection. Long clicks select more items.
+ */
+ class SelectionMode implements Mode
+ {
+ @Override
+ public void onItemClick(int position)
+ {
+ toggleSelection(position);
+ notifyListener();
+ }
+
+ @Override
+ public boolean onItemLongClick(int position)
+ {
+ toggleSelection(position);
+ notifyListener();
+ return true;
+ }
+
+ @Override
+ public void startDrag(int position)
+ {
+ toggleSelection(position);
+ notifyListener();
+ }
+
+ protected void notifyListener()
+ {
+ if (selectionListener == null) return;
+
+ if (activeMode == SELECTION_MODE)
+ selectionListener.onSelectionChange();
+ else selectionListener.onSelectionFinish();
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/package-info.java
new file mode 100644
index 000000000..c5b148812
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides controllers that are specific for {@link org.isoron.uhabits.activities.habits.list.ListHabitsActivity}.
+ */
+package org.isoron.uhabits.activities.habits.list.controllers;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java
new file mode 100644
index 000000000..71480dee6
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java
@@ -0,0 +1,276 @@
+/*
+ * 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.model;
+
+import android.support.annotation.*;
+import android.support.v7.widget.*;
+import android.view.*;
+
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.list.*;
+import org.isoron.uhabits.activities.habits.list.views.*;
+import org.isoron.uhabits.models.*;
+
+import java.util.*;
+
+import javax.inject.*;
+
+/**
+ * Provides data that backs a {@link HabitCardListView}.
+ *
+ * The data if fetched and cached by a {@link HabitCardListCache}. This adapter
+ * also holds a list of items that have been selected.
+ */
+@ActivityScope
+public class HabitCardListAdapter
+ extends RecyclerView.Adapter
+ implements HabitCardListCache.Listener
+{
+ @NonNull
+ private ModelObservable observable;
+
+ @Nullable
+ private HabitCardListView listView;
+
+ @NonNull
+ private final LinkedList selected;
+
+ @NonNull
+ private final HabitCardListCache cache;
+
+ @Inject
+ public HabitCardListAdapter(@NonNull HabitCardListCache cache)
+ {
+ this.selected = new LinkedList<>();
+ this.observable = new ModelObservable();
+ this.cache = cache;
+
+ cache.setListener(this);
+ cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
+
+ setHasStableIds(true);
+ }
+
+ public void cancelRefresh()
+ {
+ cache.cancelTasks();
+ }
+
+ /**
+ * Sets all items as not selected.
+ */
+ public void clearSelection()
+ {
+ selected.clear();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Returns the item that occupies a certain position on the list
+ *
+ * @param position position of the item
+ * @return the item at given position
+ * @throws IndexOutOfBoundsException if position is not valid
+ */
+ @Deprecated
+ @NonNull
+ public Habit getItem(int position)
+ {
+ return cache.getHabitByPosition(position);
+ }
+
+ @Override
+ public int getItemCount()
+ {
+ return cache.getHabitCount();
+ }
+
+ @Override
+ public long getItemId(int position)
+ {
+ return getItem(position).getId();
+ }
+
+ @NonNull
+ public ModelObservable getObservable()
+ {
+ return observable;
+ }
+
+ @NonNull
+ public List getSelected()
+ {
+ return new LinkedList<>(selected);
+ }
+
+ /**
+ * Returns whether list of selected items is empty.
+ *
+ * @return true if selection is empty, false otherwise
+ */
+ public boolean isSelectionEmpty()
+ {
+ return selected.isEmpty();
+ }
+
+ /**
+ * Notify the adapter that it has been attached to a ListView.
+ */
+ public void onAttached()
+ {
+ cache.onAttached();
+ }
+
+ @Override
+ public void onBindViewHolder(@Nullable HabitCardViewHolder holder,
+ int position)
+ {
+ if (holder == null) return;
+ if (listView == null) return;
+
+ Habit habit = cache.getHabitByPosition(position);
+ int score = cache.getScore(habit.getId());
+ int checkmarks[] = cache.getCheckmarks(habit.getId());
+ boolean selected = this.selected.contains(habit);
+
+ listView.bindCardView(holder, habit, score, checkmarks, selected);
+ }
+
+ @Override
+ public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType)
+ {
+ if (listView == null) return null;
+ View view = listView.createCardView();
+ return new HabitCardViewHolder(view);
+ }
+
+ /**
+ * Notify the adapter that it has been detached from a ListView.
+ */
+ public void onDetached()
+ {
+ cache.onDetached();
+ }
+
+ @Override
+ public void onItemChanged(int position)
+ {
+ notifyItemChanged(position);
+ observable.notifyListeners();
+ }
+
+ @Override
+ public void onItemInserted(int position)
+ {
+ notifyItemInserted(position);
+ observable.notifyListeners();
+ }
+
+ @Override
+ public void onItemMoved(int fromPosition, int toPosition)
+ {
+ notifyItemMoved(fromPosition, toPosition);
+ observable.notifyListeners();
+ }
+
+ @Override
+ public void onItemRemoved(int position)
+ {
+ notifyItemRemoved(position);
+ observable.notifyListeners();
+ }
+
+ @Override
+ public void onRefreshFinished()
+ {
+ observable.notifyListeners();
+ }
+
+ /**
+ * Removes a list of habits from the adapter.
+ *
+ * Note that this only has effect on the adapter cache. The database is not
+ * modified, and the change is lost when the cache is refreshed. This method
+ * is useful for making the ListView more responsive: while we wait for the
+ * database operation to finish, the cache can be modified to reflect the
+ * changes immediately.
+ *
+ * @param habits list of habits to be removed
+ */
+ public void performRemove(List habits)
+ {
+ for (Habit h : habits)
+ cache.remove(h.getId());
+ }
+
+ /**
+ * Changes the order of habits on the adapter.
+ *
+ * Note that this only has effect on the adapter cache. The database is not
+ * modified, and the change is lost when the cache is refreshed. This method
+ * is useful for making the ListView more responsive: while we wait for the
+ * database operation to finish, the cache can be modified to reflect the
+ * changes immediately.
+ *
+ * @param from the habit that should be moved
+ * @param to the habit that currently occupies the desired position
+ */
+ public void performReorder(int from, int to)
+ {
+ cache.reorder(from, to);
+ }
+
+ public void refresh()
+ {
+ cache.refreshAllHabits();
+ }
+
+ public void setFilter(HabitMatcher matcher)
+ {
+ cache.setFilter(matcher);
+ }
+
+ /**
+ * Sets the HabitCardListView that this adapter will provide data for.
+ *
+ * This object will be used to generated new HabitCardViews, upon demand.
+ *
+ * @param listView the HabitCardListView associated with this adapter
+ */
+ public void setListView(@Nullable HabitCardListView listView)
+ {
+ this.listView = listView;
+ }
+
+ /**
+ * Selects or deselects the item at a given position.
+ *
+ * @param position position of the item to be toggled
+ */
+ public void toggleSelection(int position)
+ {
+ Habit h = getItem(position);
+ int k = selected.indexOf(h);
+ if (k < 0) selected.add(h);
+ else selected.remove(h);
+ notifyDataSetChanged();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java
new file mode 100644
index 000000000..56356ec3f
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java
@@ -0,0 +1,396 @@
+/*
+ * 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.model;
+
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.commands.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import javax.inject.*;
+
+/**
+ * A HabitCardListCache fetches and keeps a cache of all the data necessary to
+ * render a HabitCardListView.
+ *
+ * This is needed since performing database lookups during scrolling can make
+ * the ListView very slow. It also registers itself as an observer of the
+ * models, in order to update itself automatically.
+ *
+ * Note that this class is singleton-scoped, therefore it is shared among all
+ * activities.
+ */
+@AppScope
+public class HabitCardListCache implements CommandRunner.Listener
+{
+ private int checkmarkCount;
+
+ private Task currentFetchTask;
+
+ @NonNull
+ private Listener listener;
+
+ @NonNull
+ private CacheData data;
+
+ @NonNull
+ private HabitList allHabits;
+
+ @NonNull
+ private HabitList filteredHabits;
+
+ private final TaskRunner taskRunner;
+
+ private final CommandRunner commandRunner;
+
+ @Inject
+ public HabitCardListCache(@NonNull HabitList allHabits,
+ @NonNull CommandRunner commandRunner,
+ @NonNull TaskRunner taskRunner)
+ {
+ this.allHabits = allHabits;
+ this.commandRunner = commandRunner;
+ this.filteredHabits = allHabits;
+ this.taskRunner = taskRunner;
+
+ this.listener = new Listener() {};
+ data = new CacheData();
+ }
+
+ public void cancelTasks()
+ {
+ if (currentFetchTask != null) currentFetchTask.cancel();
+ }
+
+ public int[] getCheckmarks(long habitId)
+ {
+ return data.checkmarks.get(habitId);
+ }
+
+ /**
+ * Returns the habits that occupies a certain position on the list.
+ *
+ * @param position the position of the habit
+ * @return the habit at given position
+ * @throws IndexOutOfBoundsException if position is not valid
+ */
+ @NonNull
+ public Habit getHabitByPosition(int position)
+ {
+ return data.habits.get(position);
+ }
+
+ public int getHabitCount()
+ {
+ return data.habits.size();
+ }
+
+ public int getScore(long habitId)
+ {
+ return data.scores.get(habitId);
+ }
+
+ public void onAttached()
+ {
+ refreshAllHabits();
+ commandRunner.addListener(this);
+ }
+
+ @Override
+ public void onCommandExecuted(@NonNull Command command,
+ @Nullable Long refreshKey)
+ {
+ if (refreshKey == null) refreshAllHabits();
+ else refreshHabit(refreshKey);
+ }
+
+ public void onDetached()
+ {
+ commandRunner.removeListener(this);
+ }
+
+ public void refreshAllHabits()
+ {
+ if (currentFetchTask != null) currentFetchTask.cancel();
+ currentFetchTask = new RefreshTask();
+ taskRunner.execute(currentFetchTask);
+ }
+
+ public void refreshHabit(long id)
+ {
+ taskRunner.execute(new RefreshTask(id));
+ }
+
+ public void remove(@NonNull Long id)
+ {
+ Habit h = data.id_to_habit.get(id);
+ if (h == null) return;
+
+ int position = data.habits.indexOf(h);
+ data.habits.remove(position);
+ data.id_to_habit.remove(id);
+ data.checkmarks.remove(id);
+ data.scores.remove(id);
+
+ listener.onItemRemoved(position);
+ }
+
+ public void reorder(int from, int to)
+ {
+ Habit fromHabit = data.habits.get(from);
+ data.habits.remove(from);
+ data.habits.add(to, fromHabit);
+ listener.onItemMoved(from, to);
+ }
+
+ public void setCheckmarkCount(int checkmarkCount)
+ {
+ this.checkmarkCount = checkmarkCount;
+ }
+
+ public void setFilter(HabitMatcher matcher)
+ {
+ filteredHabits = allHabits.getFiltered(matcher);
+ }
+
+ public void setListener(@NonNull Listener listener)
+ {
+ this.listener = listener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the data on the
+ * cache has been modified.
+ */
+ public interface Listener
+ {
+ default void onItemChanged(int position) {}
+
+ default void onItemInserted(int position) {}
+
+ default void onItemMoved(int oldPosition, int newPosition) {}
+
+ default void onItemRemoved(int position) {}
+
+ default void onRefreshFinished() {}
+ }
+
+ private class CacheData
+ {
+ @NonNull
+ public HashMap id_to_habit;
+
+ @NonNull
+ public List habits;
+
+ @NonNull
+ public HashMap checkmarks;
+
+ @NonNull
+ public HashMap scores;
+
+ /**
+ * Creates a new CacheData without any content.
+ */
+ public CacheData()
+ {
+ id_to_habit = new HashMap<>();
+ habits = new LinkedList<>();
+ checkmarks = new HashMap<>();
+ scores = new HashMap<>();
+ }
+
+ public void copyCheckmarksFrom(@NonNull CacheData oldData)
+ {
+ int[] empty = new int[checkmarkCount];
+
+ for (Long id : id_to_habit.keySet())
+ {
+ if (oldData.checkmarks.containsKey(id))
+ checkmarks.put(id, oldData.checkmarks.get(id));
+ else checkmarks.put(id, empty);
+ }
+ }
+
+ public void copyScoresFrom(@NonNull CacheData oldData)
+ {
+ for (Long id : id_to_habit.keySet())
+ {
+ if (oldData.scores.containsKey(id))
+ scores.put(id, oldData.scores.get(id));
+ else scores.put(id, 0);
+ }
+ }
+
+ public void fetchHabits()
+ {
+ for (Habit h : filteredHabits)
+ {
+ habits.add(h);
+ id_to_habit.put(h.getId(), h);
+ }
+ }
+ }
+
+ private class RefreshTask implements Task
+ {
+ @NonNull
+ private CacheData newData;
+
+ @Nullable
+ private Long targetId;
+
+ private boolean isCancelled;
+
+ private TaskRunner runner;
+
+ public RefreshTask()
+ {
+ newData = new CacheData();
+ targetId = null;
+ isCancelled = false;
+ }
+
+ public RefreshTask(long targetId)
+ {
+ newData = new CacheData();
+ this.targetId = targetId;
+ }
+
+ @Override
+ public void cancel()
+ {
+ isCancelled = true;
+ }
+
+ @Override
+ public void doInBackground()
+ {
+ newData.fetchHabits();
+ newData.copyScoresFrom(data);
+ newData.copyCheckmarksFrom(data);
+
+ long day = DateUtils.millisecondsInOneDay;
+ long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
+ long dateFrom = dateTo - (checkmarkCount - 1) * day;
+
+ runner.publishProgress(this, -1);
+
+ for (int position = 0; position < newData.habits.size(); position++)
+ {
+ if (isCancelled) return;
+
+ Habit habit = newData.habits.get(position);
+ Long id = habit.getId();
+ if (targetId != null && !targetId.equals(id)) continue;
+
+ newData.scores.put(id, habit.getScores().getTodayValue());
+ newData.checkmarks.put(id,
+ habit.getCheckmarks().getValues(dateFrom, dateTo));
+
+ runner.publishProgress(this, position);
+ }
+ }
+
+ @Override
+ public void onAttached(@NonNull TaskRunner runner)
+ {
+ this.runner = runner;
+ }
+
+ @Override
+ public void onPostExecute()
+ {
+ currentFetchTask = null;
+ listener.onRefreshFinished();
+ }
+
+ @Override
+ public void onProgressUpdate(int currentPosition)
+ {
+ if (currentPosition < 0) processRemovedHabits();
+ else processPosition(currentPosition);
+ }
+
+ private void performInsert(Habit habit, int position)
+ {
+ Long id = habit.getId();
+ data.habits.add(position, habit);
+ data.id_to_habit.put(id, habit);
+ data.scores.put(id, newData.scores.get(id));
+ data.checkmarks.put(id, newData.checkmarks.get(id));
+ listener.onItemInserted(position);
+ }
+
+ private void performMove(Habit habit, int fromPosition, int toPosition)
+ {
+ data.habits.remove(fromPosition);
+ data.habits.add(toPosition, habit);
+ listener.onItemMoved(fromPosition, toPosition);
+ }
+
+ private void performUpdate(Long id, int position)
+ {
+ Integer oldScore = data.scores.get(id);
+ int[] oldCheckmarks = data.checkmarks.get(id);
+
+ Integer newScore = newData.scores.get(id);
+ int[] newCheckmarks = newData.checkmarks.get(id);
+
+ boolean unchanged = true;
+ if (!oldScore.equals(newScore)) unchanged = false;
+ if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false;
+ if (unchanged) return;
+
+ data.scores.put(id, newScore);
+ data.checkmarks.put(id, newCheckmarks);
+ listener.onItemChanged(position);
+ }
+
+ private void processPosition(int currentPosition)
+ {
+ Habit habit = newData.habits.get(currentPosition);
+ Long id = habit.getId();
+
+ int prevPosition = data.habits.indexOf(habit);
+
+ if (prevPosition < 0) performInsert(habit, currentPosition);
+ else if (prevPosition == currentPosition)
+ performUpdate(id, currentPosition);
+ else performMove(habit, prevPosition, currentPosition);
+ }
+
+ private void processRemovedHabits()
+ {
+ Set before = data.id_to_habit.keySet();
+ Set after = newData.id_to_habit.keySet();
+
+ Set removed = new TreeSet<>(before);
+ removed.removeAll(after);
+
+ for (Long id : removed) remove(id);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java
new file mode 100644
index 000000000..494801e48
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java
@@ -0,0 +1,31 @@
+/*
+ * 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.model;
+
+import android.support.v7.widget.*;
+import android.view.*;
+
+public class HabitCardViewHolder extends RecyclerView.ViewHolder
+{
+ public HabitCardViewHolder(View itemView)
+ {
+ super(itemView);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HintList.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HintList.java
new file mode 100644
index 000000000..8fa0d6274
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HintList.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model;
+
+import android.support.annotation.*;
+
+import com.google.auto.factory.*;
+
+import org.isoron.uhabits.preferences.*;
+import org.isoron.uhabits.utils.*;
+
+/**
+ * Provides a list of hints to be shown at the application startup, and takes
+ * care of deciding when a new hint should be shown.
+ */
+@AutoFactory
+public class HintList
+{
+ private final Preferences prefs;
+
+ @NonNull
+ private final String[] hints;
+
+ /**
+ * Constructs a new list containing the provided hints.
+ *
+ * @param hints initial list of hints
+ */
+ public HintList(@Provided @NonNull Preferences prefs,
+ @NonNull String hints[])
+ {
+ this.prefs = prefs;
+ this.hints = hints;
+ }
+
+ /**
+ * Returns a new hint to be shown to the user.
+ *
+ * The hint returned is marked as read on the list, and will not be returned
+ * again. In case all hints have already been read, and there is nothing
+ * left, returns null.
+ *
+ * @return the next hint to be shown, or null if none
+ */
+ public String pop()
+ {
+ int next = prefs.getLastHintNumber() + 1;
+ if (next >= hints.length) return null;
+
+ prefs.updateLastHint(next, DateUtils.getStartOfToday());
+ return hints[next];
+ }
+
+ /**
+ * Returns whether it is time to show a new hint or not.
+ *
+ * @return true if hint should be shown, false otherwise
+ */
+ public boolean shouldShow()
+ {
+ long lastHintTimestamp = prefs.getLastHintTimestamp();
+ return (DateUtils.getStartOfToday() > lastHintTimestamp);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/package-info.java
new file mode 100644
index 000000000..755ffcaa1
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides models that are specific for {@link org.isoron.uhabits.activities.habits.list.ListHabitsActivity}.
+ */
+package org.isoron.uhabits.activities.habits.list.model;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/package-info.java
new file mode 100644
index 000000000..1a39e29de
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides acitivity for listing habits and related classes.
+ */
+package org.isoron.uhabits.activities.habits.list;
\ No newline at end of file
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
new file mode 100644
index 000000000..1e1f98a38
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java
@@ -0,0 +1,111 @@
+/*
+ * 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.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+public class CheckmarkButtonView extends TextView
+{
+ private int color;
+
+ private int value;
+
+ private StyledResources res;
+
+ public CheckmarkButtonView(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public void setColor(int color)
+ {
+ this.color = color;
+ postInvalidate();
+ }
+
+ public void setController(final CheckmarkButtonController controller)
+ {
+ setOnClickListener(v -> controller.onClick());
+ setOnLongClickListener(v -> controller.onLongClick());
+ }
+
+ public void setValue(int value)
+ {
+ this.value = value;
+ updateText();
+ }
+
+ public void toggle()
+ {
+ value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED :
+ Checkmark.CHECKED_EXPLICITLY);
+
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ updateText();
+ }
+
+ private void init()
+ {
+ res = new StyledResources(getContext());
+
+ setWillNotDraw(false);
+ setHapticFeedbackEnabled(false);
+
+ setMinHeight(
+ getResources().getDimensionPixelSize(R.dimen.checkmarkHeight));
+ setMinWidth(
+ getResources().getDimensionPixelSize(R.dimen.checkmarkWidth));
+
+ setFocusable(false);
+ setGravity(Gravity.CENTER);
+ setTypeface(InterfaceUtils.getFontAwesome(getContext()));
+ }
+
+ private void updateText()
+ {
+ int lowContrastColor = res.getColor(R.attr.lowContrastTextColor);
+
+ if (value == Checkmark.CHECKED_EXPLICITLY)
+ {
+ setText(R.string.fa_check);
+ setTextColor(color);
+ }
+
+ if (value == Checkmark.CHECKED_IMPLICITLY)
+ {
+ setText(R.string.fa_check);
+ setTextColor(lowContrastColor);
+ }
+
+ if (value == Checkmark.UNCHECKED)
+ {
+ setText(R.string.fa_times);
+ setTextColor(lowContrastColor);
+ }
+ }
+}
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
new file mode 100644
index 000000000..e5b47800c
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java
@@ -0,0 +1,208 @@
+/*
+ * 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 static android.view.View.MeasureSpec.*;
+
+public class CheckmarkPanelView extends LinearLayout implements Preferences.Listener
+{
+ private static final int CHECKMARK_LEFT_TO_RIGHT = 0;
+
+ private static final int CHECKMARK_RIGHT_TO_LEFT = 1;
+
+ @Nullable
+ private Preferences prefs;
+
+ private int checkmarkValues[];
+
+ private int nButtons;
+
+ private int color;
+
+ private Controller controller;
+
+ @NonNull
+ private Habit habit;
+
+ public CheckmarkPanelView(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public CheckmarkPanelView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ public CheckmarkButtonView indexToButton(int i)
+ {
+ int position = i;
+
+ if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT)
+ position = nButtons - i - 1;
+
+ return (CheckmarkButtonView) getChildAt(position);
+ }
+
+ public void setCheckmarkValues(int[] checkmarkValues)
+ {
+ this.checkmarkValues = checkmarkValues;
+
+ if (this.nButtons != checkmarkValues.length)
+ {
+ this.nButtons = checkmarkValues.length;
+ addCheckmarkButtons();
+ }
+
+ setupCheckmarkButtons();
+ }
+
+ public void setColor(int color)
+ {
+ this.color = color;
+ setupCheckmarkButtons();
+ }
+
+ public void setController(Controller controller)
+ {
+ this.controller = controller;
+ setupCheckmarkButtons();
+ }
+
+ public void setHabit(@NonNull Habit habit)
+ {
+ this.habit = habit;
+ setupCheckmarkButtons();
+ }
+
+ @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 addCheckmarkButtons()
+ {
+ removeAllViews();
+
+ for (int i = 0; i < nButtons; i++)
+ addView(new CheckmarkButtonView(getContext()));
+ }
+
+ private int getCheckmarkOrder()
+ {
+ if (prefs == null) return CHECKMARK_LEFT_TO_RIGHT;
+ return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT :
+ CHECKMARK_LEFT_TO_RIGHT;
+ }
+
+ private void init()
+ {
+ Context appContext = getContext().getApplicationContext();
+ if(appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ prefs = app.getComponent().getPreferences();
+ }
+
+ setWillNotDraw(false);
+ }
+
+ private void setupButtonControllers(long timestamp,
+ CheckmarkButtonView buttonView)
+ {
+ if (controller == null) return;
+ if (!(getContext() instanceof ListHabitsActivity)) return;
+
+ ListHabitsActivity activity = (ListHabitsActivity) getContext();
+ CheckmarkButtonControllerFactory buttonControllerFactory = activity
+ .getListHabitsComponent()
+ .getCheckmarkButtonControllerFactory();
+
+ CheckmarkButtonController buttonController =
+ buttonControllerFactory.create(habit, timestamp);
+ buttonController.setListener(controller);
+ buttonController.setView(buttonView);
+ buttonView.setController(buttonController);
+ }
+
+ private void setupCheckmarkButtons()
+ {
+ long timestamp = DateUtils.getStartOfToday();
+ long day = DateUtils.millisecondsInOneDay;
+
+ for (int i = 0; i < nButtons; i++)
+ {
+ CheckmarkButtonView buttonView = indexToButton(i);
+ buttonView.setValue(checkmarkValues[i]);
+ 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/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java
new file mode 100644
index 000000000..b193da2c9
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java
@@ -0,0 +1,222 @@
+/*
+ * 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.support.v7.widget.*;
+import android.support.v7.widget.helper.*;
+import android.util.*;
+import android.view.*;
+
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.activities.habits.list.controllers.*;
+import org.isoron.uhabits.activities.habits.list.model.*;
+
+import java.util.*;
+
+public class HabitCardListView extends RecyclerView
+{
+ @Nullable
+ private HabitCardListAdapter adapter;
+
+ @Nullable
+ private Controller controller;
+
+ private final ItemTouchHelper touchHelper;
+
+ private int checkmarkCount;
+
+ public HabitCardListView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ setLongClickable(true);
+ setHasFixedSize(true);
+ setLayoutManager(new LinearLayoutManager(getContext()));
+
+ TouchHelperCallback callback = new TouchHelperCallback();
+ touchHelper = new ItemTouchHelper(callback);
+ touchHelper.attachToRecyclerView(this);
+ }
+
+ /**
+ * Builds a new HabitCardView to be eventually added to this list,
+ * containing the given data.
+ *
+ * @param holder the ViewHolder containing the HabitCardView that should
+ * be built
+ * @param habit the habit for this card
+ * @param score the current score for the habit
+ * @param checkmarks the list of checkmark values to be included in the
+ * card
+ * @param selected true if the card is selected, false otherwise
+ * @return the HabitCardView generated
+ */
+ public View bindCardView(@NonNull HabitCardViewHolder holder,
+ @NonNull Habit habit,
+ int score,
+ int[] checkmarks,
+ boolean selected)
+ {
+ int visibleCheckmarks[] =
+ Arrays.copyOfRange(checkmarks, 0, checkmarkCount);
+
+ HabitCardView cardView = (HabitCardView) holder.itemView;
+ cardView.setHabit(habit);
+ cardView.setSelected(selected);
+ cardView.setCheckmarkValues(visibleCheckmarks);
+ cardView.setScore(score);
+ if (controller != null) setupCardViewController(holder);
+ return cardView;
+ }
+
+ public View createCardView()
+ {
+ return new HabitCardView(getContext());
+ }
+
+ @Override
+ public void setAdapter(RecyclerView.Adapter adapter)
+ {
+ this.adapter = (HabitCardListAdapter) adapter;
+ super.setAdapter(adapter);
+ }
+
+ public void setCheckmarkCount(int checkmarkCount)
+ {
+ this.checkmarkCount = checkmarkCount;
+ }
+
+ public void setController(@Nullable Controller controller)
+ {
+ this.controller = controller;
+ }
+
+ @Override
+ protected void onAttachedToWindow()
+ {
+ super.onAttachedToWindow();
+ if (adapter != null) adapter.onAttached();
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ if (adapter != null) adapter.onDetached();
+ super.onDetachedFromWindow();
+ }
+
+ protected void setupCardViewController(@NonNull HabitCardViewHolder holder)
+ {
+ HabitCardView cardView = (HabitCardView) holder.itemView;
+ HabitCardController cardController = new HabitCardController();
+ cardController.setListener(controller);
+ cardView.setController(cardController);
+ cardController.setView(cardView);
+
+ GestureDetector detector = new GestureDetector(getContext(),
+ new CardViewGestureDetector(holder));
+
+ cardView.setOnTouchListener((v, ev) -> {
+ detector.onTouchEvent(ev);
+ return true;
+ });
+ }
+
+ public interface Controller
+ extends CheckmarkButtonController.Listener, HabitCardController.Listener
+ {
+ void drop(int from, int to);
+
+ void onItemClick(int pos);
+
+ void onItemLongClick(int pos);
+
+ void startDrag(int position);
+ }
+
+ private class CardViewGestureDetector
+ extends GestureDetector.SimpleOnGestureListener
+ {
+ @NonNull
+ private final HabitCardViewHolder holder;
+
+ public CardViewGestureDetector(@NonNull HabitCardViewHolder holder)
+ {
+ this.holder = holder;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e)
+ {
+ int position = holder.getAdapterPosition();
+ if (controller != null) controller.onItemLongClick(position);
+ touchHelper.startDrag(holder);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e)
+ {
+ int position = holder.getAdapterPosition();
+ if (controller != null) controller.onItemClick(position);
+ return true;
+ }
+ }
+
+ class TouchHelperCallback extends ItemTouchHelper.Callback
+ {
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView,
+ ViewHolder viewHolder)
+ {
+ int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+ int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView,
+ ViewHolder from,
+ ViewHolder to)
+ {
+ if (controller == null) return false;
+ controller.drop(from.getAdapterPosition(), to.getAdapterPosition());
+ return true;
+ }
+
+ @Override
+ public void onSwiped(ViewHolder viewHolder, int direction)
+ {
+ // NOP
+ }
+ }
+}
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
new file mode 100644
index 000000000..abcebb79f
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java
@@ -0,0 +1,247 @@
+/*
+ * 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.annotation.*;
+import android.content.*;
+import android.graphics.drawable.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import butterknife.*;
+
+import static android.os.Build.VERSION.*;
+import static android.os.Build.VERSION_CODES.*;
+
+public class HabitCardView extends FrameLayout
+ implements ModelObservable.Listener
+{
+
+ private static final String EDIT_MODE_HABITS[] = {
+ "Wake up early",
+ "Wash dishes",
+ "Exercise",
+ "Meditate",
+ "Play guitar",
+ "Wash clothes",
+ "Get a haircut"
+ };
+
+ @BindView(R.id.checkmarkPanel)
+ CheckmarkPanelView checkmarkPanel;
+
+ @BindView(R.id.innerFrame)
+ LinearLayout innerFrame;
+
+ @BindView(R.id.label)
+ TextView label;
+
+ @BindView(R.id.scoreRing)
+ RingView scoreRing;
+
+ private final Context context = getContext();
+
+ private StyledResources res;
+
+ @Nullable
+ private Habit habit;
+
+ public HabitCardView(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public HabitCardView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ public void onModelChange()
+ {
+ new Handler(Looper.getMainLooper()).post(() -> refresh());
+ }
+
+ public void setCheckmarkValues(int checkmarks[])
+ {
+ checkmarkPanel.setCheckmarkValues(checkmarks);
+ postInvalidate();
+ }
+
+ public void setController(Controller controller)
+ {
+ checkmarkPanel.setController(null);
+ if (controller == null) return;
+ checkmarkPanel.setController(controller);
+ }
+
+ public void setHabit(@NonNull Habit habit)
+ {
+ if (this.habit != null) detachFromHabit();
+
+ this.habit = habit;
+ checkmarkPanel.setHabit(habit);
+ refresh();
+
+ attachToHabit();
+ postInvalidate();
+ }
+
+ public void setScore(int score)
+ {
+ float percentage = (float) score / Score.MAX_VALUE;
+ scoreRing.setPercentage(percentage);
+ scoreRing.setPrecision(1.0f / 16);
+ postInvalidate();
+ }
+
+ @Override
+ public void setSelected(boolean isSelected)
+ {
+ super.setSelected(isSelected);
+ updateBackground(isSelected);
+ }
+
+ public void triggerRipple(long timestamp)
+ {
+ long today = DateUtils.getStartOfToday();
+ long day = DateUtils.millisecondsInOneDay;
+ int offset = (int) ((today - timestamp) / day);
+ CheckmarkButtonView button = checkmarkPanel.indexToButton(offset);
+
+ float y = button.getHeight() / 2.0f;
+ float x = checkmarkPanel.getX() + button.getX() + button.getWidth() / 2;
+ triggerRipple(x, y);
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ if (habit != null) detachFromHabit();
+ super.onDetachedFromWindow();
+ }
+
+ private void attachToHabit()
+ {
+ if (habit != null) habit.getObservable().addListener(this);
+ }
+
+ private void detachFromHabit()
+ {
+ if (habit != null) habit.getObservable().removeListener(this);
+ }
+
+ private int getActiveColor(Habit habit)
+ {
+ int mediumContrastColor = res.getColor(R.attr.mediumContrastTextColor);
+ int activeColor = ColorUtils.getColor(context, habit.getColor());
+ if (habit.isArchived()) activeColor = mediumContrastColor;
+
+ return activeColor;
+ }
+
+ private void init()
+ {
+ setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT));
+
+ res = new StyledResources(getContext());
+
+ inflate(context, R.layout.list_habits_card, this);
+ ButterKnife.bind(this);
+
+ innerFrame.setOnTouchListener((v, event) -> {
+ if (SDK_INT >= LOLLIPOP)
+ v.getBackground().setHotspot(event.getX(), event.getY());
+ return false;
+ });
+
+ if (isInEditMode()) initEditMode();
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void initEditMode()
+ {
+ 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.setCheckmarkValues(values);
+ }
+
+ private void refresh()
+ {
+ int color = getActiveColor(habit);
+ label.setText(habit.getName());
+ label.setTextColor(color);
+ scoreRing.setColor(color);
+ checkmarkPanel.setColor(color);
+ postInvalidate();
+ }
+
+ private void triggerRipple(final float x, final float y)
+ {
+ final Drawable background = innerFrame.getBackground();
+ if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y);
+ background.setState(new int[]{
+ android.R.attr.state_pressed, android.R.attr.state_enabled
+ });
+ new Handler().postDelayed(() -> background.setState(new int[]{}), 25);
+ }
+
+ private void updateBackground(boolean isSelected)
+ {
+ if (SDK_INT >= LOLLIPOP)
+ {
+ if (isSelected)
+ innerFrame.setBackgroundResource(R.drawable.selected_box);
+ else innerFrame.setBackgroundResource(R.drawable.ripple);
+ }
+ else
+ {
+ Drawable background;
+
+ if (isSelected)
+ background = res.getDrawable(R.attr.selectedBackground);
+ else background = res.getDrawable(R.attr.cardBackground);
+
+ innerFrame.setBackgroundDrawable(background);
+ }
+ }
+
+ public interface Controller extends CheckmarkPanelView.Controller {}
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java
new file mode 100644
index 000000000..70ed6ec91
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java
@@ -0,0 +1,113 @@
+/*
+ * 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.view.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.preferences.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+public class HeaderView extends LinearLayout implements Preferences.Listener
+{
+ private final Context context;
+
+ private int buttonCount;
+
+ @Nullable
+ private Preferences prefs;
+
+ public HeaderView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ this.context = context;
+
+ if (isInEditMode())
+ {
+ setButtonCount(5);
+ }
+
+ Context appContext = context.getApplicationContext();
+ if (appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ prefs = app.getComponent().getPreferences();
+ }
+ }
+
+ @Override
+ public void onCheckmarkOrderChanged()
+ {
+ createButtons();
+ }
+
+ public void setButtonCount(int buttonCount)
+ {
+ this.buttonCount = buttonCount;
+ createButtons();
+ }
+
+ @Override
+ protected void onAttachedToWindow()
+ {
+ super.onAttachedToWindow();
+ if (prefs != null) prefs.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ if (prefs != null) prefs.removeListener(this);
+ super.onDetachedFromWindow();
+ }
+
+ private void createButtons()
+ {
+ removeAllViews();
+ GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
+
+ for (int i = 0; i < buttonCount; i++)
+ addView(
+ inflate(context, R.layout.list_habits_header_checkmark, null));
+
+ for (int i = 0; i < getChildCount(); i++)
+ {
+ int position = i;
+ if (shouldReverseCheckmarks()) position = getChildCount() - i - 1;
+
+ View button = getChildAt(position);
+ TextView label = (TextView) button.findViewById(R.id.tvCheck);
+ label.setText(DateUtils.formatHeaderDate(day));
+ day.add(GregorianCalendar.DAY_OF_MONTH, -1);
+ }
+ }
+
+ private boolean shouldReverseCheckmarks()
+ {
+ if (prefs == null) return false;
+ return prefs.shouldReverseCheckmarks();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HintView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HintView.java
new file mode 100644
index 000000000..f083b6fd2
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HintView.java
@@ -0,0 +1,134 @@
+/*
+ * 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.animation.AnimatorListenerAdapter;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.isoron.uhabits.R;
+import org.isoron.uhabits.activities.habits.list.model.HintList;
+
+import java.util.Random;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+public class HintView extends FrameLayout
+{
+ @BindView(R.id.hintContent)
+ TextView hintContent;
+
+ @Nullable
+ private HintList hintList;
+
+ public HintView(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public HintView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ public void onAttachedToWindow()
+ {
+ super.onAttachedToWindow();
+ showNext();
+ }
+
+ /**
+ * Sets the list of hints to be shown
+ *
+ * @param hintList the list of hints to be shown
+ */
+ public void setHints(@Nullable HintList hintList)
+ {
+ this.hintList = hintList;
+ }
+
+ private void dismiss()
+ {
+ animate().alpha(0f).setDuration(500).setListener(new DismissAnimator());
+ }
+
+ private void init()
+ {
+ addView(inflate(getContext(), R.layout.list_habits_hint, null));
+ ButterKnife.bind(this);
+
+ setVisibility(GONE);
+ setClickable(true);
+ setOnClickListener(v -> dismiss());
+
+ if (isInEditMode()) initEditMode();
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void initEditMode()
+ {
+ String hints[] = {
+ "Cats are the most popular pet in the United States: There " +
+ "are 88 million pet cats and 74 million dogs.",
+ "A cat has been mayor of Talkeetna, Alaska, for 15 years. " +
+ "His name is Stubbs.",
+ "Cats can’t taste sweetness."
+ };
+
+ int k = new Random().nextInt(hints.length);
+ hintContent.setText(hints[k]);
+ setVisibility(VISIBLE);
+ setAlpha(1.0f);
+ }
+
+ protected void showNext()
+ {
+ if (hintList == null) return;
+ if (!hintList.shouldShow()) return;
+
+ String hint = hintList.pop();
+ if (hint == null) return;
+
+ hintContent.setText(hint);
+ requestLayout();
+
+ setAlpha(0.0f);
+ setVisibility(View.VISIBLE);
+ animate().alpha(1f).setDuration(500);
+ }
+
+ private class DismissAnimator extends AnimatorListenerAdapter
+ {
+ @Override
+ public void onAnimationEnd(android.animation.Animator animation)
+ {
+ setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.java
new file mode 100644
index 000000000..7d9a90eef
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.java
@@ -0,0 +1,74 @@
+/*
+ * 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.show;
+
+import android.content.*;
+import android.net.*;
+import android.os.*;
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.models.*;
+
+/**
+ * Activity that allows the user to see more information about a single habit.
+ *
+ * Shows all the metadata for the habit, in addition to several charts.
+ */
+public class ShowHabitActivity extends BaseActivity
+{
+ private HabitList habits;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ HabitsApplication app = (HabitsApplication) getApplicationContext();
+ habits = app.getComponent().getHabitList();
+ Habit habit = getHabitFromIntent();
+
+ ShowHabitComponent component = DaggerShowHabitComponent
+ .builder()
+ .appComponent(app.getComponent())
+ .showHabitModule(new ShowHabitModule(this, habit))
+ .build();
+
+ ShowHabitRootView rootView = component.getRootView();
+ ShowHabitScreen screen = component.getScreen();
+
+ setScreen(screen);
+ screen.setMenu(component.getMenu());
+ screen.setController(component.getController());
+ rootView.setController(component.getController());
+
+ screen.reattachDialogs();
+ }
+
+ @NonNull
+ private Habit getHabitFromIntent()
+ {
+ Uri data = getIntent().getData();
+ Habit habit = habits.getById(ContentUris.parseId(data));
+ if (habit == null) throw new RuntimeException("habit not found");
+ return habit;
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitComponent.java
new file mode 100644
index 000000000..e1974c23e
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitComponent.java
@@ -0,0 +1,39 @@
+/*
+ * 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.show;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+
+import dagger.*;
+
+@ActivityScope
+@Component(modules = { ShowHabitModule.class },
+ dependencies = { AppComponent.class })
+public interface ShowHabitComponent
+{
+ ShowHabitController getController();
+
+ ShowHabitsMenu getMenu();
+
+ ShowHabitRootView getRootView();
+
+ ShowHabitScreen getScreen();
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java
new file mode 100644
index 000000000..c4a463199
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java
@@ -0,0 +1,72 @@
+/*
+ * 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.show;
+
+import android.support.annotation.*;
+
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.common.dialogs.*;
+import org.isoron.uhabits.commands.*;
+import org.isoron.uhabits.models.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ShowHabitController
+ implements ShowHabitRootView.Controller, HistoryEditorDialog.Controller
+{
+ @NonNull
+ private final ShowHabitScreen screen;
+
+ @NonNull
+ private final Habit habit;
+
+ @NonNull
+ private final CommandRunner commandRunner;
+
+ @Inject
+ public ShowHabitController(@NonNull ShowHabitScreen screen,
+ @NonNull CommandRunner commandRunner,
+ @NonNull Habit habit)
+ {
+ this.screen = screen;
+ this.habit = habit;
+ this.commandRunner = commandRunner;
+ }
+
+ @Override
+ public void onEditHistoryButtonClick()
+ {
+ screen.showEditHistoryDialog();
+ }
+
+ @Override
+ public void onToggleCheckmark(long timestamp)
+ {
+ commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp),
+ null);
+ }
+
+ @Override
+ public void onToolbarChanged()
+ {
+ screen.invalidateToolbar();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java
new file mode 100644
index 000000000..bf4cbee25
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java
@@ -0,0 +1,43 @@
+/*
+ * 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.show;
+
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.models.*;
+
+import dagger.*;
+
+@Module
+public class ShowHabitModule extends ActivityModule
+{
+ private final Habit habit;
+
+ public ShowHabitModule(BaseActivity activity, Habit habit)
+ {
+ super(activity);
+ this.habit = habit;
+ }
+
+ @Provides
+ public Habit getHabit()
+ {
+ return habit;
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java
new file mode 100644
index 000000000..ffeb3ee42
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java
@@ -0,0 +1,158 @@
+/*
+ * 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.show;
+
+import android.content.*;
+import android.os.*;
+import android.support.annotation.*;
+import android.support.v7.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.habits.show.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import javax.inject.*;
+
+import butterknife.*;
+
+@ActivityScope
+public class ShowHabitRootView extends BaseRootView
+ implements ModelObservable.Listener
+{
+ @NonNull
+ private Habit habit;
+
+ @BindView(R.id.frequencyCard)
+ FrequencyCard frequencyCard;
+
+ @BindView(R.id.streakCard)
+ StreakCard streakCard;
+
+ @BindView(R.id.subtitleCard)
+ SubtitleCard subtitleCard;
+
+ @BindView(R.id.overviewCard)
+ OverviewCard overviewCard;
+
+ @BindView(R.id.scoreCard)
+ ScoreCard scoreCard;
+
+ @BindView(R.id.historyCard)
+ HistoryCard historyCard;
+
+ @BindView(R.id.toolbar)
+ Toolbar toolbar;
+
+ @NonNull
+ private Controller controller;
+
+ @Inject
+ public ShowHabitRootView(@NonNull @ActivityContext Context context,
+ @NonNull Habit habit)
+ {
+ super(context);
+ this.habit = habit;
+
+ addView(inflate(getContext(), R.layout.show_habit, null));
+ ButterKnife.bind(this);
+
+ controller = new Controller() {};
+
+ initCards();
+ initToolbar();
+ }
+
+ @Override
+ public boolean getDisplayHomeAsUp()
+ {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public Toolbar getToolbar()
+ {
+ return toolbar;
+ }
+
+ @Override
+ public int getToolbarColor()
+ {
+ StyledResources res = new StyledResources(getContext());
+ if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
+ return super.getToolbarColor();
+
+ return ColorUtils.getColor(getContext(), habit.getColor());
+ }
+
+ @Override
+ public void onModelChange()
+ {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ toolbar.setTitle(habit.getName());
+ });
+
+ controller.onToolbarChanged();
+ }
+
+ public void setController(@NonNull Controller controller)
+ {
+ this.controller = controller;
+ historyCard.setController(controller);
+ }
+
+ @Override
+ protected void initToolbar()
+ {
+ super.initToolbar();
+ toolbar.setTitle(habit.getName());
+ }
+
+ @Override
+ protected void onAttachedToWindow()
+ {
+ super.onAttachedToWindow();
+ habit.getObservable().addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ habit.getObservable().removeListener(this);
+ super.onDetachedFromWindow();
+ }
+
+ private void initCards()
+ {
+ subtitleCard.setHabit(habit);
+ overviewCard.setHabit(habit);
+ scoreCard.setHabit(habit);
+ historyCard.setHabit(habit);
+ streakCard.setHabit(habit);
+ frequencyCard.setHabit(habit);
+ }
+
+ public interface Controller extends HistoryCard.Controller
+ {
+ default void onToolbarChanged() {}
+ }
+}
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
new file mode 100644
index 000000000..1c238c02e
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java
@@ -0,0 +1,87 @@
+/*
+ * 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.show;
+
+import android.support.annotation.*;
+
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.activities.common.dialogs.*;
+import org.isoron.uhabits.activities.habits.edit.*;
+import org.isoron.uhabits.models.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ShowHabitScreen extends BaseScreen
+{
+ @NonNull
+ private final Habit habit;
+
+ @Nullable
+ private ShowHabitController controller;
+
+ @NonNull
+ private final EditHabitDialogFactory editHabitDialogFactory;
+
+ @Inject
+ public ShowHabitScreen(@NonNull BaseActivity activity,
+ @NonNull Habit habit,
+ @NonNull ShowHabitRootView view,
+ @NonNull EditHabitDialogFactory editHabitDialogFactory)
+ {
+ super(activity);
+ setRootView(view);
+ this.editHabitDialogFactory = editHabitDialogFactory;
+ this.habit = habit;
+ }
+
+ public void setController(@NonNull ShowHabitController controller)
+ {
+ this.controller = controller;
+ }
+
+ public void reattachDialogs()
+ {
+ if(controller == null) throw new IllegalStateException();
+
+ HistoryEditorDialog historyEditor = (HistoryEditorDialog) activity
+ .getSupportFragmentManager()
+ .findFragmentByTag("historyEditor");
+
+ if (historyEditor != null)
+ historyEditor.setController(controller);
+ }
+
+ public void showEditHabitDialog()
+ {
+ EditHabitDialog dialog = editHabitDialogFactory.create(habit);
+ activity.showDialog(dialog, "editHabit");
+ }
+
+ public void showEditHistoryDialog()
+ {
+ if(controller == null) throw new IllegalStateException();
+
+ HistoryEditorDialog dialog = new HistoryEditorDialog();
+ dialog.setHabit(habit);
+ dialog.setController(controller);
+ dialog.show(activity.getSupportFragmentManager(), "historyEditor");
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java
new file mode 100644
index 000000000..92e5af582
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java
@@ -0,0 +1,63 @@
+/*
+ * 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.show;
+
+import android.support.annotation.*;
+import android.view.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+
+import javax.inject.*;
+
+@ActivityScope
+public class ShowHabitsMenu extends BaseMenu
+{
+ @NonNull
+ private final ShowHabitScreen screen;
+
+ @Inject
+ public ShowHabitsMenu(@NonNull BaseActivity activity,
+ @NonNull ShowHabitScreen screen)
+ {
+ super(activity);
+ this.screen = screen;
+ }
+
+ @Override
+ public boolean onItemSelected(@NonNull MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.action_edit_habit:
+ screen.showEditHabitDialog();
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected int getMenuResourceId()
+ {
+ return R.menu.show_habit;
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/package-info.java
new file mode 100644
index 000000000..ca132a6c7
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides activity that displays detailed habit information and related
+ * classes.
+ */
+package org.isoron.uhabits.activities.habits.show;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java
new file mode 100644
index 000000000..f9438efe3
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java
@@ -0,0 +1,109 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import butterknife.*;
+
+public class FrequencyCard extends HabitCard
+{
+ @BindView(R.id.title)
+ TextView title;
+
+ @BindView(R.id.frequencyChart)
+ FrequencyChart chart;
+
+ @Nullable
+ private TaskRunner taskRunner;
+
+ public FrequencyCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public FrequencyCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ if(taskRunner == null) return;
+ taskRunner.execute(new RefreshTask());
+ }
+
+ private void init()
+ {
+ inflate(getContext(), R.layout.show_habit_frequency, this);
+ ButterKnife.bind(this);
+
+ Context appContext = getContext().getApplicationContext();
+ if(appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ taskRunner = app.getComponent().getTaskRunner();
+ }
+
+ if (isInEditMode()) initEditMode();
+ }
+
+ private void initEditMode()
+ {
+ int color = ColorUtils.getAndroidTestColor(1);
+ title.setTextColor(color);
+ chart.setColor(color);
+ chart.populateWithRandomData();
+ }
+
+ private class RefreshTask implements Task
+ {
+ @Override
+ public void doInBackground()
+ {
+ RepetitionList reps = getHabit().getRepetitions();
+ HashMap frequency = reps.getWeekdayFrequency();
+ chart.setFrequency(frequency);
+ }
+
+ @Override
+ public void onPreExecute()
+ {
+ int paletteColor = getHabit().getColor();
+ int color = ColorUtils.getColor(getContext(), paletteColor);
+ title.setTextColor(color);
+ chart.setColor(color);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java
new file mode 100644
index 000000000..0c31112d6
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java
@@ -0,0 +1,103 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.models.memory.*;
+
+public abstract class HabitCard extends LinearLayout
+ implements ModelObservable.Listener
+{
+ @NonNull
+ private Habit habit;
+
+ public HabitCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public HabitCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @NonNull
+ public Habit getHabit()
+ {
+ return habit;
+ }
+
+ public void setHabit(@NonNull Habit habit)
+ {
+ detachFrom(this.habit);
+ attachTo(habit);
+
+ this.habit = habit;
+ }
+
+ @Override
+ public void onModelChange()
+ {
+ post(() -> refreshData());
+ }
+
+ @Override
+ protected void onAttachedToWindow()
+ {
+ if(isInEditMode()) return;
+
+ super.onAttachedToWindow();
+ refreshData();
+ attachTo(habit);
+ }
+
+ @Override
+ protected void onDetachedFromWindow()
+ {
+ detachFrom(habit);
+ super.onDetachedFromWindow();
+ }
+
+ protected abstract void refreshData();
+
+ private void attachTo(Habit habit)
+ {
+ habit.getObservable().addListener(this);
+ habit.getRepetitions().getObservable().addListener(this);
+ }
+
+ private void detachFrom(Habit habit)
+ {
+ habit.getRepetitions().getObservable().removeListener(this);
+ habit.getObservable().removeListener(this);
+ }
+
+ private void init()
+ {
+ if(!isInEditMode()) habit = new MemoryModelFactory().buildHabit();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java
new file mode 100644
index 000000000..4e060b990
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java
@@ -0,0 +1,130 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import butterknife.*;
+
+public class HistoryCard extends HabitCard
+{
+ @BindView(R.id.historyChart)
+ HistoryChart chart;
+
+ @BindView(R.id.title)
+ TextView title;
+
+ @NonNull
+ private Controller controller;
+
+ @Nullable
+ private TaskRunner taskRunner;
+
+ public HistoryCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public HistoryCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @OnClick(R.id.edit)
+ public void onClickEditButton()
+ {
+ controller.onEditHistoryButtonClick();
+ }
+
+ public void setController(@NonNull Controller controller)
+ {
+ this.controller = controller;
+ chart.setController(controller);
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ if(taskRunner == null) return;
+ taskRunner.execute(new RefreshTask(getHabit()));
+ }
+
+ private void init()
+ {
+ inflate(getContext(), R.layout.show_habit_history, this);
+ ButterKnife.bind(this);
+
+ Context appContext = getContext().getApplicationContext();
+ if (appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ taskRunner = app.getComponent().getTaskRunner();
+ }
+
+ controller = new Controller() {};
+ if (isInEditMode()) initEditMode();
+ }
+
+ private void initEditMode()
+ {
+ int color = ColorUtils.getAndroidTestColor(1);
+ title.setTextColor(color);
+ chart.setColor(color);
+ chart.populateWithRandomData();
+ }
+
+ public interface Controller extends HistoryChart.Controller
+ {
+ default void onEditHistoryButtonClick() {}
+ }
+
+ private class RefreshTask implements Task
+ {
+ private final Habit habit;
+
+ public RefreshTask(Habit habit) {this.habit = habit;}
+
+ @Override
+ public void doInBackground()
+ {
+ int checkmarks[] = habit.getCheckmarks().getAllValues();
+ chart.setCheckmarks(checkmarks);
+ }
+
+ @Override
+ public void onPreExecute()
+ {
+ int color = ColorUtils.getColor(getContext(), habit.getColor());
+ title.setTextColor(color);
+ chart.setColor(color);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java
new file mode 100644
index 000000000..24eebec29
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java
@@ -0,0 +1,189 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import butterknife.*;
+
+public class OverviewCard extends HabitCard
+{
+ @NonNull
+ private Cache cache;
+
+ @BindView(R.id.scoreRing)
+ RingView scoreRing;
+
+ @BindView(R.id.scoreLabel)
+ TextView scoreLabel;
+
+ @BindView(R.id.monthDiffLabel)
+ TextView monthDiffLabel;
+
+ @BindView(R.id.yearDiffLabel)
+ TextView yearDiffLabel;
+
+ @BindView(R.id.totalCountLabel)
+ TextView totalCountLabel;
+
+ @BindView(R.id.title)
+ TextView title;
+
+ private int color;
+
+ @Nullable
+ private TaskRunner taskRunner;
+
+ public OverviewCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public OverviewCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ if(taskRunner == null) return;
+ taskRunner.execute(new RefreshTask());
+ }
+
+ private String formatPercentageDiff(float percentageDiff)
+ {
+ return String.format("%s%.0f%%", (percentageDiff >= 0 ? "+" : "\u2212"),
+ Math.abs(percentageDiff) * 100);
+ }
+
+ private void init()
+ {
+ Context appContext = getContext().getApplicationContext();
+ if (appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ taskRunner = app.getComponent().getTaskRunner();
+ }
+
+ inflate(getContext(), R.layout.show_habit_overview, this);
+ ButterKnife.bind(this);
+ cache = new Cache();
+
+ if (isInEditMode()) initEditMode();
+ }
+
+ private void initEditMode()
+ {
+ color = ColorUtils.getAndroidTestColor(1);
+ cache.todayScore = Score.MAX_VALUE * 0.6f;
+ cache.lastMonthScore = Score.MAX_VALUE * 0.42f;
+ cache.lastYearScore = Score.MAX_VALUE * 0.75f;
+ refreshColors();
+ refreshScore();
+ }
+
+ private void refreshColors()
+ {
+ scoreRing.setColor(color);
+ scoreLabel.setTextColor(color);
+ title.setTextColor(color);
+ }
+
+ private void refreshScore()
+ {
+ float todayPercentage = cache.todayScore / Score.MAX_VALUE;
+ float monthDiff =
+ todayPercentage - (cache.lastMonthScore / Score.MAX_VALUE);
+ float yearDiff =
+ todayPercentage - (cache.lastYearScore / Score.MAX_VALUE);
+
+ scoreRing.setPercentage(todayPercentage);
+ scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100));
+
+ monthDiffLabel.setText(formatPercentageDiff(monthDiff));
+ yearDiffLabel.setText(formatPercentageDiff(yearDiff));
+ totalCountLabel.setText(String.valueOf(cache.totalCount));
+
+ StyledResources res = new StyledResources(getContext());
+ int inactiveColor = res.getColor(R.attr.mediumContrastTextColor);
+
+ monthDiffLabel.setTextColor(monthDiff >= 0 ? color : inactiveColor);
+ yearDiffLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor);
+ totalCountLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor);
+
+ postInvalidate();
+ }
+
+ private class Cache
+ {
+ public float todayScore;
+
+ public float lastMonthScore;
+
+ public float lastYearScore;
+
+ public long totalCount;
+ }
+
+ private class RefreshTask implements Task
+ {
+ @Override
+ public void doInBackground()
+ {
+ Habit habit = getHabit();
+
+ ScoreList scores = habit.getScores();
+
+ long today = DateUtils.getStartOfToday();
+ long lastMonth = today - 30 * DateUtils.millisecondsInOneDay;
+ long lastYear = today - 365 * DateUtils.millisecondsInOneDay;
+
+ cache.todayScore = (float) scores.getTodayValue();
+ cache.lastMonthScore = (float) scores.getValue(lastMonth);
+ cache.lastYearScore = (float) scores.getValue(lastYear);
+ cache.totalCount = habit.getRepetitions().getTotalCount();
+ }
+
+ @Override
+ public void onPostExecute()
+ {
+ refreshScore();
+ }
+
+ @Override
+ public void onPreExecute()
+ {
+ color = ColorUtils.getColor(getContext(), getHabit().getColor());
+ refreshColors();
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java
new file mode 100644
index 000000000..f228c00dd
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java
@@ -0,0 +1,165 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.preferences.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import butterknife.*;
+
+public class ScoreCard extends HabitCard
+{
+ public static final int[] BUCKET_SIZES = { 1, 7, 31, 92, 365 };
+
+ @BindView(R.id.spinner)
+ Spinner spinner;
+
+ @BindView(R.id.scoreView)
+ ScoreChart chart;
+
+ @BindView(R.id.title)
+ TextView title;
+
+ private int bucketSize;
+
+ @Nullable
+ private TaskRunner taskRunner;
+
+ @Nullable
+ private Preferences prefs;
+
+ public ScoreCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public ScoreCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @NonNull
+ public static DateUtils.TruncateField getTruncateField(int bucketSize)
+ {
+ if (bucketSize == 7) return DateUtils.TruncateField.WEEK_NUMBER;
+ if (bucketSize == 31) return DateUtils.TruncateField.MONTH;
+ if (bucketSize == 92) return DateUtils.TruncateField.QUARTER;
+ if (bucketSize == 365) return DateUtils.TruncateField.YEAR;
+
+ Log.e("ScoreCard",
+ String.format("Unknown bucket size: %d", bucketSize));
+
+ return DateUtils.TruncateField.MONTH;
+ }
+
+ @OnItemSelected(R.id.spinner)
+ public void onItemSelected(int position)
+ {
+ setBucketSizeFromPosition(position);
+ HabitsApplication app =
+ (HabitsApplication) getContext().getApplicationContext();
+ app.getComponent().getWidgetUpdater().updateWidgets();
+ refreshData();
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ if(taskRunner == null) return;
+ taskRunner.execute(new RefreshTask());
+ }
+
+ private int getDefaultSpinnerPosition()
+ {
+ if(prefs == null) return 0;
+ return prefs.getDefaultScoreSpinnerPosition();
+ }
+
+ private void init()
+ {
+ Context appContext = getContext().getApplicationContext();
+ if (appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ taskRunner = app.getComponent().getTaskRunner();
+ prefs = app.getComponent().getPreferences();
+ }
+
+ inflate(getContext(), R.layout.show_habit_score, this);
+ ButterKnife.bind(this);
+
+ int defaultPosition = getDefaultSpinnerPosition();
+ setBucketSizeFromPosition(defaultPosition);
+ spinner.setSelection(defaultPosition);
+
+ if (isInEditMode())
+ {
+ spinner.setVisibility(GONE);
+ title.setTextColor(ColorUtils.getAndroidTestColor(1));
+ chart.setColor(ColorUtils.getAndroidTestColor(1));
+ chart.populateWithRandomData();
+ }
+ }
+
+ private void setBucketSizeFromPosition(int position)
+ {
+ if(prefs == null) return;
+ prefs.setDefaultScoreSpinnerPosition(position);
+ bucketSize = BUCKET_SIZES[position];
+ }
+
+ private class RefreshTask implements Task
+ {
+ @Override
+ public void doInBackground()
+ {
+ List scores;
+ ScoreList scoreList = getHabit().getScores();
+
+ if (bucketSize == 1) scores = scoreList.toList();
+ else scores = scoreList.groupBy(getTruncateField(bucketSize));
+
+ chart.setScores(scores);
+ chart.setBucketSize(bucketSize);
+ }
+
+ @Override
+ public void onPreExecute()
+ {
+ int color =
+ ColorUtils.getColor(getContext(), getHabit().getColor());
+ title.setTextColor(color);
+ chart.setColor(color);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java
new file mode 100644
index 000000000..3389217fb
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java
@@ -0,0 +1,118 @@
+/*
+ * 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.show.views;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.common.views.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.tasks.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import butterknife.*;
+
+public class StreakCard extends HabitCard
+{
+ public static final int NUM_STREAKS = 10;
+
+ @BindView(R.id.title)
+ TextView title;
+
+ @BindView(R.id.streakChart)
+ StreakChart streakChart;
+
+ @Nullable
+ private TaskRunner taskRunner;
+
+ public StreakCard(Context context)
+ {
+ super(context);
+ init();
+ }
+
+ public StreakCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ if(taskRunner == null) return;
+ taskRunner.execute(new RefreshTask());
+ }
+
+ private void init()
+ {
+ Context appContext = getContext().getApplicationContext();
+ if (appContext instanceof HabitsApplication)
+ {
+ HabitsApplication app = (HabitsApplication) appContext;
+ taskRunner = app.getComponent().getTaskRunner();
+ }
+
+ inflate(getContext(), R.layout.show_habit_streak, this);
+ ButterKnife.bind(this);
+ setOrientation(VERTICAL);
+ if (isInEditMode()) initEditMode();
+ }
+
+ private void initEditMode()
+ {
+ int color = ColorUtils.getAndroidTestColor(1);
+ title.setTextColor(color);
+ streakChart.setColor(color);
+ streakChart.populateWithRandomData();
+ }
+
+ private class RefreshTask implements Task
+ {
+ public List bestStreaks;
+
+ @Override
+ public void doInBackground()
+ {
+ StreakList streaks = getHabit().getStreaks();
+ bestStreaks = streaks.getBest(NUM_STREAKS);
+ }
+
+ @Override
+ public void onPostExecute()
+ {
+ streakChart.setStreaks(bestStreaks);
+ }
+
+ @Override
+ public void onPreExecute()
+ {
+ int color =
+ ColorUtils.getColor(getContext(), getHabit().getColor());
+ title.setTextColor(color);
+ streakChart.setColor(color);
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java
new file mode 100644
index 000000000..4d033473e
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java
@@ -0,0 +1,114 @@
+/*
+ * 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.show.views;
+
+import android.annotation.*;
+import android.content.*;
+import android.content.res.*;
+import android.util.*;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import butterknife.*;
+
+public class SubtitleCard extends HabitCard
+{
+ @BindView(R.id.questionLabel)
+ TextView questionLabel;
+
+ @BindView(R.id.frequencyLabel)
+ TextView frequencyLabel;
+
+ @BindView(R.id.reminderLabel)
+ TextView reminderLabel;
+
+ public SubtitleCard(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ protected void refreshData()
+ {
+ Habit habit = getHabit();
+ int color = ColorUtils.getColor(getContext(), habit.getColor());
+
+ reminderLabel.setText(getResources().getString(R.string.reminder_off));
+ questionLabel.setVisibility(VISIBLE);
+
+ questionLabel.setTextColor(color);
+ questionLabel.setText(habit.getDescription());
+ frequencyLabel.setText(toText(habit.getFrequency()));
+
+ if (habit.hasReminder()) updateReminderText(habit.getReminder());
+
+ if (habit.getDescription().isEmpty()) questionLabel.setVisibility(GONE);
+
+ invalidate();
+ }
+
+ private void init()
+ {
+ inflate(getContext(), R.layout.show_habit_subtitle, this);
+ ButterKnife.bind(this);
+
+ if (isInEditMode()) initEditMode();
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void initEditMode()
+ {
+ questionLabel.setTextColor(ColorUtils.getAndroidTestColor(1));
+ questionLabel.setText("Have you meditated today?");
+ reminderLabel.setText("08:00");
+ }
+
+ private String toText(Frequency freq)
+ {
+ Resources resources = getResources();
+ Integer num = freq.getNumerator();
+ Integer den = freq.getDenominator();
+
+ if (num.equals(den)) return resources.getString(R.string.every_day);
+
+ if (num == 1)
+ {
+ if (den == 7) return resources.getString(R.string.every_week);
+ if (den % 7 == 0)
+ return resources.getString(R.string.every_x_weeks, den / 7);
+ return resources.getString(R.string.every_x_days, den);
+ }
+
+ String times_every = resources.getString(R.string.times_every);
+ return String.format("%d %s %d %s", num, times_every, den,
+ resources.getString(R.string.days));
+ }
+
+ private void updateReminderText(Reminder reminder)
+ {
+ reminderLabel.setText(
+ DateUtils.formatTime(getContext(), reminder.getHour(),
+ reminder.getMinute()));
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/package-info.java
new file mode 100644
index 000000000..f9fc65e4b
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides custom views that are used primarily on {@link
+ * org.isoron.uhabits.activities.habits.show.ShowHabitActivity}.
+ */
+package org.isoron.uhabits.activities.habits.show.views;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/IntroActivity.java b/app/src/main/java/org/isoron/uhabits/activities/intro/IntroActivity.java
similarity index 68%
rename from app/src/main/java/org/isoron/uhabits/IntroActivity.java
rename to app/src/main/java/org/isoron/uhabits/activities/intro/IntroActivity.java
index e298d1c63..4fe80138c 100644
--- a/app/src/main/java/org/isoron/uhabits/IntroActivity.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/intro/IntroActivity.java
@@ -17,14 +17,19 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits;
+package org.isoron.uhabits.activities.intro;
-import android.graphics.Color;
-import android.os.Bundle;
+import android.graphics.*;
+import android.os.*;
-import com.github.paolorotolo.appintro.AppIntro2;
-import com.github.paolorotolo.appintro.AppIntroFragment;
+import com.github.paolorotolo.appintro.*;
+import org.isoron.uhabits.R;
+
+/**
+ * Activity that introduces the app to the user, shown only after the app is
+ * launched for the first time.
+ */
public class IntroActivity extends AppIntro2
{
@Override
@@ -33,16 +38,16 @@ public class IntroActivity extends AppIntro2
showStatusBar(false);
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_1),
- getString(R.string.intro_description_1), R.drawable.intro_icon_1,
- Color.parseColor("#194673")));
+ getString(R.string.intro_description_1), R.drawable.intro_icon_1,
+ Color.parseColor("#194673")));
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_2),
- getString(R.string.intro_description_2), R.drawable.intro_icon_2,
- Color.parseColor("#ffa726")));
+ getString(R.string.intro_description_2), R.drawable.intro_icon_2,
+ Color.parseColor("#ffa726")));
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4),
- getString(R.string.intro_description_4), R.drawable.intro_icon_4,
- Color.parseColor("#9575cd")));
+ getString(R.string.intro_description_4), R.drawable.intro_icon_4,
+ Color.parseColor("#9575cd")));
}
@Override
diff --git a/app/src/main/java/org/isoron/uhabits/activities/intro/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/intro/package-info.java
new file mode 100644
index 000000000..4023d1b94
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/intro/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides activity that introduces app to the user and related classes.
+ */
+package org.isoron.uhabits.activities.intro;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/activities/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/package-info.java
new file mode 100644
index 000000000..abc95467c
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides classes for the Android activites.
+ */
+package org.isoron.uhabits.activities;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java
similarity index 51%
rename from app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java
rename to app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java
index 8be54b586..914e7e714 100644
--- a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java
@@ -17,48 +17,35 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits;
+package org.isoron.uhabits.activities.settings;
-import android.content.ContentUris;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
+import android.os.*;
-import org.isoron.uhabits.helpers.ColorHelper;
-import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.utils.*;
-public class ShowHabitActivity extends BaseActivity
+/**
+ * Activity that allows the user to view and modify the app settings.
+ */
+public class SettingsActivity extends BaseActivity
{
- private Habit habit;
-
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
-
- Uri data = getIntent().getData();
- habit = Habit.get(ContentUris.parseId(data));
-
- setContentView(R.layout.show_habit_activity);
-
- setupSupportActionBar(true);
- setupHabitActionBar();
+ setContentView(R.layout.settings_activity);
+ setupActionBarColor();
}
- private void setupHabitActionBar()
+ private void setupActionBarColor()
{
- if(habit == null) return;
+ StyledResources res = new StyledResources(this);
+ int color = BaseScreen.getDefaultActionBarColor(this);
- ActionBar actionBar = getSupportActionBar();
- if(actionBar == null) return;
+ if (res.getBoolean(R.attr.useHabitColorAsPrimary))
+ color = res.getColor(R.attr.aboutScreenColor);
- actionBar.setTitle(habit.name);
-
- setupActionBarColor(ColorHelper.getColor(this, habit.color));
- }
-
- public Habit getHabit()
- {
- return habit;
+ BaseScreen.setupActionBarColor(this, color);
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java
similarity index 55%
rename from app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java
rename to app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java
index 29ab76a4c..9b71bc153 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java
@@ -17,124 +17,121 @@
* with this program. If not, see .
*/
-package org.isoron.uhabits.fragments;
+package org.isoron.uhabits.activities.settings;
-import android.app.backup.BackupManager;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.support.v7.preference.Preference;
-import android.support.v7.preference.PreferenceCategory;
-import android.support.v7.preference.PreferenceFragmentCompat;
+import android.app.backup.*;
+import android.content.*;
+import android.os.*;
+import android.support.v7.preference.*;
-import org.isoron.uhabits.MainActivity;
import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.ReminderHelper;
-import org.isoron.uhabits.helpers.UIHelper;
+import org.isoron.uhabits.activities.habits.list.*;
+import org.isoron.uhabits.utils.*;
public class SettingsFragment extends PreferenceFragmentCompat
- implements SharedPreferences.OnSharedPreferenceChangeListener
+ implements SharedPreferences.OnSharedPreferenceChangeListener
{
private static int RINGTONE_REQUEST_CODE = 1;
+ private SharedPreferences prefs;
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ if (requestCode == RINGTONE_REQUEST_CODE)
+ {
+ RingtoneUtils.parseRingtoneData(getContext(), data);
+ updateRingtoneDescription();
+ return;
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
- setResultOnPreferenceClick("importData", MainActivity.RESULT_IMPORT_DATA);
- setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_CSV);
- setResultOnPreferenceClick("exportDB", MainActivity.RESULT_EXPORT_DB);
- setResultOnPreferenceClick("bugReport", MainActivity.RESULT_BUG_REPORT);
+ setResultOnPreferenceClick("importData", ListHabitsScreen.RESULT_IMPORT_DATA);
+ setResultOnPreferenceClick("exportCSV", ListHabitsScreen.RESULT_EXPORT_CSV);
+ setResultOnPreferenceClick("exportDB", ListHabitsScreen.RESULT_EXPORT_DB);
+ setResultOnPreferenceClick("repairDB", ListHabitsScreen.RESULT_REPAIR_DB);
+ setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT);
updateRingtoneDescription();
- if(UIHelper.isLocaleFullyTranslated())
+ if (InterfaceUtils.isLocaleFullyTranslated())
removePreference("translate", "linksCategory");
}
@Override
public void onCreatePreferences(Bundle bundle, String s)
{
-
+ // NOP
}
- private void removePreference(String preferenceKey, String categoryKey)
+ @Override
+ public void onPause()
{
- PreferenceCategory cat = (PreferenceCategory) findPreference(categoryKey);
- Preference pref = findPreference(preferenceKey);
- cat.removePreference(pref);
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ super.onPause();
}
- private void setResultOnPreferenceClick(String key, final int result)
+ @Override
+ public boolean onPreferenceTreeClick(Preference preference)
{
- Preference pref = findPreference(key);
- pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener()
+ String key = preference.getKey();
+ if (key == null) return false;
+
+ if (key.equals("reminderSound"))
{
- @Override
- public boolean onPreferenceClick(Preference preference)
- {
- getActivity().setResult(result);
- getActivity().finish();
- return true;
- }
- });
+ RingtoneUtils.startRingtonePickerActivity(this,
+ RINGTONE_REQUEST_CODE);
+ return true;
+ }
+
+ return super.onPreferenceTreeClick(preference);
}
@Override
public void onResume()
{
super.onResume();
- getPreferenceManager().getSharedPreferences().
- registerOnSharedPreferenceChangeListener(this);
+ prefs = getPreferenceManager().getSharedPreferences();
+ prefs.registerOnSharedPreferenceChangeListener(this);
}
@Override
- public void onPause()
- {
- getPreferenceManager().getSharedPreferences().
- unregisterOnSharedPreferenceChangeListener(this);
- super.onPause();
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key)
{
BackupManager.dataChanged("org.isoron.uhabits");
}
- @Override
- public boolean onPreferenceTreeClick(Preference preference)
+ private void removePreference(String preferenceKey, String categoryKey)
{
- if(preference.getKey() == null) return false;
-
- if (preference.getKey().equals("reminderSound"))
- {
- ReminderHelper.startRingtonePickerActivity(this, RINGTONE_REQUEST_CODE);
- return true;
- }
-
- return super.onPreferenceTreeClick(preference);
+ PreferenceCategory cat =
+ (PreferenceCategory) findPreference(categoryKey);
+ Preference pref = findPreference(preferenceKey);
+ cat.removePreference(pref);
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data)
+ private void setResultOnPreferenceClick(String key, final int result)
{
- if(requestCode == RINGTONE_REQUEST_CODE)
- {
- ReminderHelper.parseRingtoneData(getContext(), data);
- updateRingtoneDescription();
- return;
- }
-
- super.onActivityResult(requestCode, resultCode, data);
+ Preference pref = findPreference(key);
+ pref.setOnPreferenceClickListener(preference -> {
+ getActivity().setResult(result);
+ getActivity().finish();
+ return true;
+ });
}
private void updateRingtoneDescription()
{
- String ringtoneName = ReminderHelper.getRingtoneName(getContext());
- if(ringtoneName == null) return;
+ String ringtoneName = RingtoneUtils.getRingtoneName(getContext());
+ if (ringtoneName == null) return;
Preference ringtonePreference = findPreference("reminderSound");
ringtonePreference.setSummary(ringtoneName);
}
diff --git a/app/src/main/java/org/isoron/uhabits/activities/settings/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/settings/package-info.java
new file mode 100644
index 000000000..73ed9c7a1
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/activities/settings/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Provides activity for changing the settings.
+ */
+package org.isoron.uhabits.activities.settings;
\ No newline at end of file
diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.java
new file mode 100644
index 000000000..d92e73480
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.java
@@ -0,0 +1,50 @@
+/*
+ * 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.automation;
+
+import android.os.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.models.*;
+
+public class EditSettingActivity extends BaseActivity
+{
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ HabitsApplication app = (HabitsApplication) getApplicationContext();
+
+ HabitList habits = app.getComponent().getHabitList();
+ habits = habits.getFiltered(new HabitMatcherBuilder()
+ .setArchivedAllowed(false)
+ .setCompletedAllowed(true)
+ .build());
+
+ EditSettingController controller = new EditSettingController(this);
+ EditSettingRootView rootView =
+ new EditSettingRootView(this, habits, controller);
+
+ BaseScreen screen = new BaseScreen(this);
+ screen.setRootView(rootView);
+ setScreen(screen);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.java
new file mode 100644
index 000000000..478170496
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.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.automation;
+
+import android.app.*;
+import android.content.*;
+import android.os.*;
+import android.support.annotation.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+
+import static org.isoron.uhabits.automation.FireSettingReceiver.*;
+
+public class EditSettingController
+{
+ @NonNull
+ private final Activity activity;
+
+ public EditSettingController(@NonNull Activity activity)
+ {
+ this.activity = activity;
+ }
+
+ public void onSave(Habit habit, int action)
+ {
+ if (habit.getId() == null) return;
+
+ String actionName = getActionName(action);
+ String blurb = String.format("%s: %s", actionName, habit.getName());
+
+ Bundle bundle = new Bundle();
+ bundle.putInt("action", action);
+ bundle.putLong("habit", habit.getId());
+
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_STRING_BLURB, blurb);
+ intent.putExtra(EXTRA_BUNDLE, bundle);
+
+ activity.setResult(Activity.RESULT_OK, intent);
+ activity.finish();
+ }
+
+ private String getActionName(int action)
+ {
+ switch (action)
+ {
+ case ACTION_CHECK:
+ return activity.getString(R.string.check);
+
+ case ACTION_UNCHECK:
+ return activity.getString(R.string.uncheck);
+
+ case ACTION_TOGGLE:
+ return activity.getString(R.string.toggle);
+
+ default:
+ return "???";
+ }
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java
new file mode 100644
index 000000000..264ea8a22
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java
@@ -0,0 +1,105 @@
+/*
+ * 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.automation;
+
+import android.content.*;
+import android.support.annotation.*;
+import android.support.v7.widget.*;
+import android.support.v7.widget.Toolbar;
+import android.widget.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.activities.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.utils.*;
+
+import java.util.*;
+
+import butterknife.*;
+
+import static android.R.layout.*;
+
+public class EditSettingRootView extends BaseRootView
+{
+ @BindView(R.id.toolbar)
+ Toolbar toolbar;
+
+ @BindView(R.id.habitSpinner)
+ AppCompatSpinner habitSpinner;
+
+ @BindView(R.id.actionSpinner)
+ AppCompatSpinner actionSpinner;
+
+ @NonNull
+ private final HabitList habitList;
+
+ @NonNull
+ private final EditSettingController controller;
+
+ public EditSettingRootView(@NonNull Context context,
+ @NonNull HabitList habitList,
+ @NonNull EditSettingController controller)
+ {
+ super(context);
+ this.habitList = habitList;
+ this.controller = controller;
+
+ addView(inflate(getContext(), R.layout.automation, null));
+ ButterKnife.bind(this);
+ populateHabitSpinner();
+ }
+
+ @NonNull
+ @Override
+ public Toolbar getToolbar()
+ {
+ return toolbar;
+ }
+
+ @Override
+ public int getToolbarColor()
+ {
+ StyledResources res = new StyledResources(getContext());
+ if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
+ return super.getToolbarColor();
+
+ return res.getColor(R.attr.aboutScreenColor);
+ }
+
+ @OnClick(R.id.buttonSave)
+ public void onClickSave()
+ {
+ int action = actionSpinner.getSelectedItemPosition();
+ int habitPosition = habitSpinner.getSelectedItemPosition();
+ Habit habit = habitList.getByPosition(habitPosition);
+ controller.onSave(habit, action);
+ }
+
+ private void populateHabitSpinner()
+ {
+ List names = new LinkedList<>();
+ for (Habit h : habitList) names.add(h.getName());
+
+ ArrayAdapter adapter =
+ new ArrayAdapter<>(getContext(), simple_spinner_item, names);
+ adapter.setDropDownViewResource(simple_spinner_dropdown_item);
+ habitSpinner.setAdapter(adapter);
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java b/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java
new file mode 100644
index 000000000..7ff6af588
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java
@@ -0,0 +1,114 @@
+/*
+ * 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.automation;
+
+import android.content.*;
+import android.os.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+import org.isoron.uhabits.receivers.*;
+import org.isoron.uhabits.utils.*;
+
+import dagger.*;
+
+public class FireSettingReceiver extends BroadcastReceiver
+{
+ public static final int ACTION_CHECK = 0;
+
+ public static final int ACTION_UNCHECK = 1;
+
+ public static final int ACTION_TOGGLE = 2;
+
+ public static final String EXTRA_BUNDLE =
+ "com.twofortyfouram.locale.intent.extra.BUNDLE";
+
+ public static final String EXTRA_STRING_BLURB =
+ "com.twofortyfouram.locale.intent.extra.BLURB";
+
+ private HabitList allHabits;
+
+ @Override
+ public void onReceive(Context context, Intent intent)
+ {
+ HabitsApplication app =
+ (HabitsApplication) context.getApplicationContext();
+
+ ReceiverComponent component =
+ DaggerFireSettingReceiver_ReceiverComponent
+ .builder()
+ .appComponent(app.getComponent())
+ .build();
+
+ allHabits = app.getComponent().getHabitList();
+
+ Arguments args = parseIntent(intent);
+ if (args == null) return;
+
+ long timestamp = DateUtils.getStartOfToday();
+ WidgetController controller = component.getWidgetController();
+
+ switch (args.action)
+ {
+ case ACTION_CHECK:
+ controller.onAddRepetition(args.habit, timestamp);
+ break;
+
+ case ACTION_UNCHECK:
+ controller.onRemoveRepetition(args.habit, timestamp);
+ break;
+
+ case ACTION_TOGGLE:
+ controller.onToggleRepetition(args.habit, timestamp);
+ break;
+ }
+ }
+
+ private Arguments parseIntent(Intent intent)
+ {
+ Arguments args = new Arguments();
+
+ Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
+ if (bundle == null) return null;
+
+ args.action = bundle.getInt("action");
+ if (args.action < 0 || args.action > 2) return null;
+
+ Habit habit = allHabits.getById(bundle.getLong("habit"));
+ if (habit == null) return null;
+ args.habit = habit;
+
+ return args;
+ }
+
+ @ReceiverScope
+ @Component(dependencies = AppComponent.class)
+ interface ReceiverComponent
+ {
+ WidgetController getWidgetController();
+ }
+
+ private class Arguments
+ {
+ int action;
+
+ Habit habit;
+ }
+}
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..51993e7c7 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java
@@ -19,40 +19,49 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
-import java.util.List;
+import java.util.*;
+/**
+ * Command to archive a list of habits.
+ */
public class ArchiveHabitsCommand extends Command
{
+ private List selectedHabits;
- private List habits;
+ private final HabitList habitList;
- public ArchiveHabitsCommand(List habits)
+ public ArchiveHabitsCommand(HabitList habitList, List selectedHabits)
{
- this.habits = habits;
+ this.habitList = habitList;
+ this.selectedHabits = selectedHabits;
}
@Override
public void execute()
{
- Habit.archive(habits);
+ for (Habit h : selectedHabits) h.setArchived(true);
+ habitList.update(selectedHabits);
}
@Override
- public void undo()
- {
- Habit.unarchive(habits);
- }
-
public Integer getExecuteStringId()
{
return R.string.toast_habit_archived;
}
+ @Override
public Integer getUndoStringId()
{
return R.string.toast_habit_unarchived;
}
+
+ @Override
+ public void undo()
+ {
+ for (Habit h : selectedHabits) h.setArchived(false);
+ habitList.update(selectedHabits);
+ }
}
\ 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..503acc40f 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java
@@ -19,62 +19,60 @@
package org.isoron.uhabits.commands;
-import com.activeandroid.ActiveAndroid;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.helpers.DatabaseHelper;
-import org.isoron.uhabits.models.Habit;
-
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+/**
+ * Command to change the color of a list of habits.
+ */
public class ChangeHabitColorCommand extends Command
{
- List habits;
+ HabitList habitList;
+
+ List selected;
+
List originalColors;
+
Integer newColor;
- public ChangeHabitColorCommand(List habits, Integer newColor)
+ public ChangeHabitColorCommand(HabitList habitList,
+ List selected,
+ Integer newColor)
{
- this.habits = habits;
+ this.habitList = habitList;
+ this.selected = selected;
this.newColor = newColor;
- this.originalColors = new ArrayList<>(habits.size());
+ this.originalColors = new ArrayList<>(selected.size());
- for(Habit h : habits)
- originalColors.add(h.color);
+ for (Habit h : selected) originalColors.add(h.getColor());
}
@Override
public void execute()
{
- Habit.setColor(habits, newColor);
+ for (Habit h : selected) h.setColor(newColor);
+ habitList.update(selected);
}
@Override
- public void undo()
- {
- DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
- {
- @Override
- public void execute()
- {
- int k = 0;
- for(Habit h : habits)
- {
- h.color = originalColors.get(k++);
- h.save();
- }
- }
- });
- }
-
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
+ @Override
public Integer getUndoStringId()
{
return R.string.toast_habit_changed;
}
+
+ @Override
+ public void undo()
+ {
+ int k = 0;
+ for (Habit h : selected) h.setColor(originalColors.get(k++));
+ habitList.update(selected);
+ }
}
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..e319a5095 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/Command.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java
@@ -19,12 +19,19 @@
package org.isoron.uhabits.commands;
+/**
+ * A Command represents a desired set of changes that should be performed on the
+ * models.
+ *
+ * A command can be executed and undone. Each of these operations also provide
+ * an string that should be displayed to the user upon their completion.
+ *
+ * In general, commands should always be executed by a {@link CommandRunner}.
+ */
public abstract class Command
{
public abstract void execute();
- public abstract void undo();
-
public Integer getExecuteStringId()
{
return null;
@@ -34,4 +41,6 @@ public abstract class Command
{
return null;
}
+
+ public abstract void undo();
}
diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java
new file mode 100644
index 000000000..8ef46f6ec
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java
@@ -0,0 +1,89 @@
+/*
+ * 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.*;
+import org.isoron.uhabits.tasks.*;
+
+import java.util.*;
+
+import javax.inject.*;
+
+/**
+ * A CommandRunner executes and undoes commands.
+ *
+ * CommandRunners also allows objects to subscribe to it, and receive events
+ * whenever a command is performed.
+ */
+@AppScope
+public class CommandRunner
+{
+ private TaskRunner taskRunner;
+
+ private LinkedList listeners;
+
+ @Inject
+ public CommandRunner(@NonNull TaskRunner taskRunner)
+ {
+ this.taskRunner = taskRunner;
+ listeners = new LinkedList<>();
+ }
+
+ public void addListener(Listener l)
+ {
+ listeners.add(l);
+ }
+
+ public void execute(final Command command, final Long refreshKey)
+ {
+ taskRunner.execute(new Task()
+ {
+ @Override
+ public void doInBackground()
+ {
+ command.execute();
+ }
+
+ @Override
+ public void onPostExecute()
+ {
+ for (Listener l : listeners)
+ l.onCommandExecuted(command, refreshKey);
+ }
+ });
+ }
+
+ public void removeListener(Listener l)
+ {
+ listeners.remove(l);
+ }
+
+ /**
+ * Interface implemented by objects that want to receive an event whenever a
+ * command is executed.
+ */
+ public interface Listener
+ {
+ void onCommandExecuted(@NonNull Command command,
+ @Nullable Long refreshKey);
+ }
+}
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..51f08c482 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java
@@ -19,41 +19,45 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.models.Habit;
+import android.support.annotation.*;
+import com.google.auto.factory.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+
+/**
+ * Command to create a habit.
+ */
+@AutoFactory
public class CreateHabitCommand extends Command
{
+ private ModelFactory modelFactory;
+
+ HabitList habitList;
+
private Habit model;
+
private Long savedId;
- public CreateHabitCommand(Habit model)
+ public CreateHabitCommand(@Provided @NonNull ModelFactory modelFactory,
+ @NonNull HabitList habitList,
+ @NonNull Habit model)
{
+ this.modelFactory = modelFactory;
+ this.habitList = habitList;
this.model = model;
}
@Override
public void execute()
{
- Habit savedHabit = new Habit(model);
- if (savedId == null)
- {
- savedHabit.save();
- savedId = savedHabit.getId();
- }
- else
- {
- savedHabit.save(savedId);
- }
- }
+ Habit savedHabit = modelFactory.buildHabit();
+ savedHabit.copyFrom(model);
+ savedHabit.setId(savedId);
- @Override
- public void undo()
- {
- Habit habit = Habit.get(savedId);
- if(habit == null) throw new RuntimeException("Habit not found");
-
- habit.cascadeDelete();
+ habitList.add(savedHabit);
+ savedId = savedHabit.getId();
}
@Override
@@ -68,4 +72,13 @@ public class CreateHabitCommand extends Command
return R.string.toast_habit_deleted;
}
+ @Override
+ public void undo()
+ {
+ Habit habit = habitList.getById(savedId);
+ if (habit == null) throw new RuntimeException("Habit not found");
+
+ habitList.remove(habit);
+ }
+
}
\ 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..ca184d8bf 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java
@@ -19,42 +19,53 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
-import java.util.List;
+import java.util.*;
+/**
+ * Command to delete a list of habits.
+ */
public class DeleteHabitsCommand extends Command
{
+ HabitList habitList;
+
private List habits;
- public DeleteHabitsCommand(List habits)
+ public DeleteHabitsCommand(HabitList habitList, List habits)
{
this.habits = habits;
+ this.habitList = habitList;
}
@Override
public void execute()
{
- for(Habit h : habits)
- h.cascadeDelete();
-
- Habit.rebuildOrder();
+ for (Habit h : habits)
+ habitList.remove(h);
}
@Override
- public void undo()
+ public Integer getExecuteStringId()
{
- throw new UnsupportedOperationException();
+ return R.string.toast_habit_deleted;
}
- public Integer getExecuteStringId()
+ public List getHabits()
{
- return R.string.toast_habit_deleted;
+ return new LinkedList<>(habits);
}
+ @Override
public Integer getUndoStringId()
{
return R.string.toast_habit_restored;
}
+
+ @Override
+ public void undo()
+ {
+ throw new UnsupportedOperationException();
+ }
}
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..8d9605dbb 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
@@ -19,24 +19,45 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.models.Habit;
+import android.support.annotation.*;
+import com.google.auto.factory.*;
+
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
+
+/**
+ * Command to modify a habit.
+ */
+@AutoFactory
public class EditHabitCommand extends Command
{
+ HabitList habitList;
+
private Habit original;
+
private Habit modified;
+
private long savedId;
- private boolean hasIntervalChanged;
- public EditHabitCommand(Habit original, Habit modified)
+ private boolean hasFrequencyChanged;
+
+ public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory,
+ @NonNull HabitList habitList,
+ @NonNull Habit original,
+ @NonNull Habit modified)
{
+ this.habitList = habitList;
this.savedId = original.getId();
- this.modified = new Habit(modified);
- this.original = new Habit(original);
+ this.modified = modelFactory.buildHabit();
+ this.original = modelFactory.buildHabit();
+
+ this.modified.copyFrom(modified);
+ this.original.copyFrom(original);
- hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
- !this.original.freqNum.equals(this.modified.freqNum));
+ Frequency originalFreq = this.original.getFrequency();
+ Frequency modifiedFreq = this.modified.getFrequency();
+ hasFrequencyChanged = (!originalFreq.equals(modifiedFreq));
}
@Override
@@ -45,6 +66,18 @@ public class EditHabitCommand extends Command
copyAttributes(this.modified);
}
+ @Override
+ public Integer getExecuteStringId()
+ {
+ return R.string.toast_habit_changed;
+ }
+
+ @Override
+ public Integer getUndoStringId()
+ {
+ return R.string.toast_habit_changed_back;
+ }
+
@Override
public void undo()
{
@@ -53,32 +86,22 @@ public class EditHabitCommand extends Command
private void copyAttributes(Habit model)
{
- Habit habit = Habit.get(savedId);
- if(habit == null) throw new RuntimeException("Habit not found");
+ Habit habit = habitList.getById(savedId);
+ if (habit == null) throw new RuntimeException("Habit not found");
- habit.copyAttributes(model);
- habit.save();
+ habit.copyFrom(model);
+ habitList.update(habit);
invalidateIfNeeded(habit);
}
private void invalidateIfNeeded(Habit habit)
{
- if (hasIntervalChanged)
+ if (hasFrequencyChanged)
{
- habit.checkmarks.deleteNewerThan(0);
- habit.streaks.deleteNewerThan(0);
- habit.scores.invalidateNewerThan(0);
+ habit.getCheckmarks().invalidateNewerThan(0);
+ habit.getStreaks().invalidateNewerThan(0);
+ habit.getScores().invalidateNewerThan(0);
}
}
-
- public Integer getExecuteStringId()
- {
- return R.string.toast_habit_changed;
- }
-
- public Integer getUndoStringId()
- {
- return R.string.toast_habit_changed_back;
- }
}
\ No newline at end of file
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..5cc2fa8ba 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java
@@ -19,8 +19,11 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.models.*;
+/**
+ * Command to toggle a repetition.
+ */
public class ToggleRepetitionCommand extends Command
{
private Long offset;
@@ -35,7 +38,7 @@ public class ToggleRepetitionCommand extends Command
@Override
public void execute()
{
- habit.repetitions.toggle(offset);
+ habit.getRepetitions().toggleTimestamp(offset);
}
@Override
@@ -43,4 +46,9 @@ public class ToggleRepetitionCommand extends Command
{
execute();
}
+
+ public Habit getHabit()
+ {
+ return habit;
+ }
}
\ 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 612481fa7..6e45cda7b 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java
@@ -19,38 +19,47 @@
package org.isoron.uhabits.commands;
-import org.isoron.uhabits.R;
-import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.*;
+import org.isoron.uhabits.models.*;
-import java.util.List;
+import java.util.*;
+/**
+ * Command to unarchive a list of habits.
+ */
public class UnarchiveHabitsCommand extends Command
{
+ HabitList habitList;
private List habits;
- public UnarchiveHabitsCommand(List habits)
+ public UnarchiveHabitsCommand(HabitList habitList, List selected)
{
- this.habits = habits;
+ this.habits = selected;
+ this.habitList = habitList;
}
@Override
public void execute()
{
- Habit.unarchive(habits);
+ for(Habit h : habits) h.setArchived(false);
+ habitList.update(habits);
}
@Override
public void undo()
{
- Habit.archive(habits);
+ for(Habit h : habits) h.setArchived(true);
+ habitList.update(habits);
}
+ @Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_unarchived;
}
+ @Override
public Integer getUndoStringId()
{
return R.string.toast_habit_archived;
diff --git a/app/src/main/java/org/isoron/uhabits/commands/package-info.java b/app/src/main/java/org/isoron/uhabits/commands/package-info.java
new file mode 100644
index 000000000..8fce85ae1
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/commands/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 Álinson Santos Xavier