Merge branch 'feature/performance' into dev

pull/77/merge
Alinson S. Xavier 10 years ago
commit afc91ed89a

@ -9,7 +9,7 @@ android {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 23 targetSdkVersion 23
buildConfigField "Integer", "databaseVersion", "12" buildConfigField "Integer", "databaseVersion", "13"
buildConfigField "String", "databaseFilename", "\"uhabits.db\"" buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

@ -20,6 +20,7 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.os.Looper; import android.os.Looper;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
@ -27,7 +28,6 @@ import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.tasks.BaseTask;
import org.junit.Before; import org.junit.Before;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
public class BaseTest public class BaseTest
@ -55,6 +55,12 @@ public class BaseTest
protected void waitForAsyncTasks() throws InterruptedException, TimeoutException protected void waitForAsyncTasks() throws InterruptedException, TimeoutException
{ {
BaseTask.waitForTasks(30, TimeUnit.SECONDS); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
{
Thread.sleep(1000);
return;
}
BaseTask.waitForTasks(10000);
} }
} }

@ -19,9 +19,23 @@
package org.isoron.uhabits.unit; package org.isoron.uhabits.unit;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
import java.io.File;
import java.io.InputStream;
import java.util.Random;
import static org.junit.Assert.fail;
public class HabitFixtures public class HabitFixtures
{ {
@ -79,6 +93,74 @@ public class HabitFixtures
return habit; return habit;
} }
public static void generateHugeDataSet() throws Throwable
{
final int nHabits = 30;
final int nYears = 5;
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
@Override
public void execute()
{
Random rand = new Random();
for(int i = 0; i < nHabits; i++)
{
Log.i("HabitFixture", String.format("Creating habit %d / %d", i, nHabits));
Habit habit = new Habit();
habit.name = String.format("Habit %d", i);
habit.save();
long today = DateHelper.getStartOfToday();
long day = DateHelper.millisecondsInOneDay;
for(int j = 0; j < 365 * nYears; j++)
{
if(rand.nextBoolean())
habit.repetitions.toggle(today - j * day);
}
habit.scores.getTodayValue();
habit.streaks.getAll(1);
}
}
});
ExportDBTask task = new ExportDBTask(null);
task.setListener(new ExportDBTask.Listener()
{
@Override
public void onExportDBFinished(@Nullable String filename)
{
if(filename != null)
Log.i("HabitFixture", String.format("Huge data set exported to %s", filename));
else
Log.i("HabitFixture", "Failed to save database");
}
});
task.execute();
BaseTask.waitForTasks(30000);
}
public static void loadHugeDataSet(Context testContext) throws Throwable
{
File baseDir = DatabaseHelper.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
File dst = new File(String.format("%s/%s", baseDir.getPath(), "loopHuge.db"));
InputStream in = testContext.getAssets().open("fixtures/loopHuge.db");
DatabaseHelper.copy(in, dst);
ImportDataTask task = new ImportDataTask(dst, null);
task.execute();
BaseTask.waitForTasks(30000);
}
public static void purgeHabits() public static void purgeHabits()
{ {
for(Habit h : Habit.getAll(true)) for(Habit h : Habit.getAll(true))

@ -19,6 +19,7 @@
package org.isoron.uhabits.unit; package org.isoron.uhabits.unit;
import android.os.Build;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
@ -38,6 +39,9 @@ public class HabitsApplicationTest
@Test @Test
public void getLogcat() throws IOException public void getLogcat() throws IOException
{ {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
return;
String msg = "LOGCAT TEST"; String msg = "LOGCAT TEST";
new RuntimeException(msg).printStackTrace(); new RuntimeException(msg).printStackTrace();

@ -25,6 +25,7 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest; import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.unit.HabitFixtures; import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -37,6 +38,7 @@ import java.util.GregorianCalendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.Random; import java.util.Random;
import static junit.framework.Assert.assertFalse;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -174,4 +176,16 @@ public class RepetitionListTest extends BaseTest
from = to - 5 * DateHelper.millisecondsInOneDay; from = to - 5 * DateHelper.millisecondsInOneDay;
assertThat(habit.repetitions.count(from, to), equalTo(3)); assertThat(habit.repetitions.count(from, to), equalTo(3));
} }
@Test
public void getOldest()
{
long expectedOldestTimestamp = DateHelper.getStartOfToday() - 9 * DateHelper.millisecondsInOneDay;
assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp));
Repetition oldest = habit.repetitions.getOldest();
assertFalse(oldest == null);
assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp));
}
} }

@ -34,8 +34,6 @@ import org.junit.runner.RunWith;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;

@ -47,6 +47,7 @@ public class CheckmarkViewTest extends ViewTest
habit = HabitFixtures.createShortHabit(); habit = HabitFixtures.createShortHabit();
view = new CheckmarkView(targetContext); view = new CheckmarkView(targetContext);
view.setHabit(habit); view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(100), dpToPixels(200), view); measureView(dpToPixels(100), dpToPixels(200), view);
} }

@ -45,6 +45,7 @@ public class HabitFrequencyViewTest extends ViewTest
view = new HabitFrequencyView(targetContext); view = new HabitFrequencyView(targetContext);
view.setHabit(habit); view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view); measureView(dpToPixels(300), dpToPixels(100), view);
} }

@ -21,7 +21,6 @@ package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -54,12 +53,12 @@ public class HabitHistoryViewTest extends ViewTest
view = new HabitHistoryView(targetContext); view = new HabitHistoryView(targetContext);
view.setHabit(habit); view.setHabit(habit);
measureView(dpToPixels(300), dpToPixels(100), view); measureView(dpToPixels(300), dpToPixels(100), view);
refreshData(view);
} }
@Test @Test
public void render() throws Throwable public void render() throws Throwable
{ {
Log.d("HabitHistoryViewTest", String.format("height=%d", dpToPixels(100)));
assertRenders(view, "HabitHistoryView/render.png"); assertRenders(view, "HabitHistoryView/render.png");
} }

@ -48,6 +48,7 @@ public class HabitScoreViewTest extends ViewTest
view = new HabitScoreView(targetContext); view = new HabitScoreView(targetContext);
view.setHabit(habit); view.setHabit(habit);
view.setBucketSize(7); view.setBucketSize(7);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view); measureView(dpToPixels(300), dpToPixels(100), view);
} }

@ -21,7 +21,6 @@ package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures; import org.isoron.uhabits.unit.HabitFixtures;
@ -45,8 +44,10 @@ public class HabitStreakViewTest extends ViewTest
Habit habit = HabitFixtures.createLongHabit(); Habit habit = HabitFixtures.createLongHabit();
view = new HabitStreakView(targetContext); view = new HabitStreakView(targetContext);
view.setHabit(habit);
measureView(dpToPixels(300), dpToPixels(100), view); measureView(dpToPixels(300), dpToPixels(100), view);
view.setHabit(habit);
refreshData(view);
} }
@Test @Test
@ -66,6 +67,8 @@ public class HabitStreakViewTest extends ViewTest
public void render_withSmallSize() throws Throwable public void render_withSmallSize() throws Throwable
{ {
measureView(dpToPixels(100), dpToPixels(100), view); measureView(dpToPixels(100), dpToPixels(100), view);
refreshData(view);
assertRenders(view, "HabitStreakView/renderSmallSize.png"); assertRenders(view, "HabitStreakView/renderSmallSize.png");
} }
} }

@ -27,7 +27,9 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import org.isoron.uhabits.BaseTest; import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitDataView;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -38,7 +40,7 @@ import static junit.framework.Assert.fail;
public class ViewTest extends BaseTest public class ViewTest extends BaseTest
{ {
protected static final double SIMILARITY_CUTOFF = 0.08; protected static final double SIMILARITY_CUTOFF = 0.09;
public static final int HISTOGRAM_BIN_SIZE = 8; public static final int HISTOGRAM_BIN_SIZE = 8;
protected void measureView(int width, int height, View view) protected void measureView(int width, int height, View view)
@ -179,7 +181,7 @@ public class ViewTest extends BaseTest
protected int dpToPixels(int dp) protected int dpToPixels(int dp)
{ {
return (int) DialogHelper.dpToPixels(targetContext, dp); return (int) UIHelper.dpToPixels(targetContext, dp);
} }
protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException
@ -190,4 +192,25 @@ public class ViewTest extends BaseTest
view.onSingleTapUp(e); view.onSingleTapUp(e);
e.recycle(); e.recycle();
} }
protected void refreshData(final HabitDataView view)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
view.refreshData();
}
}.execute();
try
{
waitForAsyncTasks();
}
catch (Exception e)
{
throw new RuntimeException("Time out");
}
}
} }

@ -0,0 +1,4 @@
create index idx_score_habit_timestamp on score(habit, timestamp);
create index idx_checkmark_habit_timestamp on checkmarks(habit, timestamp);
create index idx_repetitions_habit_timestamp on repetitions(habit, timestamp);
create index idx_streak_habit_end on streak(habit, end);

@ -37,7 +37,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -85,8 +85,8 @@ public class MainActivity extends BaseActivity
private void onStartup() private void onStartup()
{ {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
DialogHelper.incrementLaunchCount(this); UIHelper.incrementLaunchCount(this);
DialogHelper.updateLastAppVersion(this); UIHelper.updateLastAppVersion(this);
showTutorial(); showTutorial();
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {

@ -20,27 +20,16 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.app.ActionBar; import android.app.ActionBar;
import android.content.BroadcastReceiver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import org.isoron.uhabits.fragments.ShowHabitFragment;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
public class ShowHabitActivity extends BaseActivity public class ShowHabitActivity extends BaseActivity
{ {
private Habit habit;
public Habit habit;
private Receiver receiver;
private LocalBroadcastManager localBroadcastManager;
private ShowHabitFragment fragment;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
@ -51,37 +40,18 @@ public class ShowHabitActivity extends BaseActivity
habit = Habit.get(ContentUris.parseId(data)); habit = Habit.get(ContentUris.parseId(data));
ActionBar actionBar = getActionBar(); ActionBar actionBar = getActionBar();
if(actionBar != null) if(actionBar != null && getHabit() != null)
{ {
actionBar.setTitle(habit.name); actionBar.setTitle(getHabit().name);
if (android.os.Build.VERSION.SDK_INT >= 21) if (android.os.Build.VERSION.SDK_INT >= 21)
actionBar.setBackgroundDrawable(new ColorDrawable(habit.color)); actionBar.setBackgroundDrawable(new ColorDrawable(getHabit().color));
} }
setContentView(R.layout.show_habit_activity); setContentView(R.layout.show_habit_activity);
fragment = (ShowHabitFragment) getFragmentManager().findFragmentById(R.id.fragment2);
receiver = new Receiver();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(receiver,
new IntentFilter(MainActivity.ACTION_REFRESH));
}
class Receiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
fragment.refreshData();
}
} }
@Override public Habit getHabit()
protected void onDestroy()
{ {
localBroadcastManager.unregisterReceiver(receiver); return habit;
super.onDestroy();
} }
} }

@ -30,6 +30,7 @@ import android.util.Log;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitHistoryView; import org.isoron.uhabits.views.HabitHistoryView;
public class HistoryEditorDialog extends DialogFragment public class HistoryEditorDialog extends DialogFragment
@ -44,7 +45,6 @@ public class HistoryEditorDialog extends DialogFragment
{ {
Context context = getActivity(); Context context = getActivity();
historyView = new HabitHistoryView(context, null); historyView = new HabitHistoryView(context, null);
int p = (int) getResources().getDimension(R.dimen.history_editor_padding);
if(savedInstanceState != null) if(savedInstanceState != null)
{ {
@ -52,7 +52,8 @@ public class HistoryEditorDialog extends DialogFragment
if(id > 0) this.habit = Habit.get(id); if(id > 0) this.habit = Habit.get(id);
} }
historyView.setPadding(p, 0, p, 0); int padding = (int) getResources().getDimension(R.dimen.history_editor_padding);
historyView.setPadding(padding, 0, padding, 0);
historyView.setHabit(habit); historyView.setHabit(habit);
historyView.setIsEditable(true); historyView.setIsEditable(true);
@ -61,9 +62,23 @@ public class HistoryEditorDialog extends DialogFragment
.setView(historyView) .setView(historyView)
.setPositiveButton(android.R.string.ok, this); .setPositiveButton(android.R.string.ok, this);
refreshData();
return builder.create(); return builder.create();
} }
private void refreshData()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
historyView.refreshData();
}
}.execute();
}
@Override @Override
public void onResume() public void onResume()
{ {

@ -42,7 +42,7 @@ import com.android.datetimepicker.time.TimePickerDialog;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener; import org.isoron.uhabits.helpers.UIHelper.OnSavedListener;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CreateHabitCommand; import org.isoron.uhabits.commands.CreateHabitCommand;

@ -36,7 +36,7 @@ import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand; import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand; import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -49,7 +49,7 @@ public class HabitSelectionCallback implements ActionMode.Callback
private List<Integer> selectedPositions; private List<Integer> selectedPositions;
private BaseActivity activity; private BaseActivity activity;
private Listener listener; private Listener listener;
private DialogHelper.OnSavedListener onSavedListener; private UIHelper.OnSavedListener onSavedListener;
private ProgressBar progressBar; private ProgressBar progressBar;
public interface Listener public interface Listener
@ -74,7 +74,7 @@ public class HabitSelectionCallback implements ActionMode.Callback
this.progressBar = progressBar; this.progressBar = progressBar;
} }
public void setOnSavedListener(DialogHelper.OnSavedListener onSavedListener) public void setOnSavedListener(UIHelper.OnSavedListener onSavedListener)
{ {
this.onSavedListener = onSavedListener; this.onSavedListener = onSavedListener;
} }

@ -56,8 +56,7 @@ import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.dialogs.FilePickerDialog; import org.isoron.uhabits.dialogs.FilePickerDialog;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper.OnSavedListener;
import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener;
import org.isoron.uhabits.helpers.HintManager; import org.isoron.uhabits.helpers.HintManager;
import org.isoron.uhabits.helpers.ListHabitsHelper; import org.isoron.uhabits.helpers.ListHabitsHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
@ -107,7 +106,9 @@ public class ListHabitsFragment extends Fragment
listView = (DragSortListView) view.findViewById(R.id.listView); listView = (DragSortListView) view.findViewById(R.id.listView);
llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader); llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader);
llEmpty = view.findViewById(R.id.llEmpty); llEmpty = view.findViewById(R.id.llEmpty);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar); progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
progressBar.setVisibility(View.GONE);
selectedPositions = new LinkedList<>(); selectedPositions = new LinkedList<>();
loader = new HabitListLoader(); loader = new HabitListLoader();
@ -116,7 +117,6 @@ public class ListHabitsFragment extends Fragment
loader.setListener(this); loader.setListener(this);
loader.setCheckmarkCount(helper.getButtonCount()); loader.setCheckmarkCount(helper.getButtonCount());
loader.setProgressBar(progressBar);
llHint.setOnClickListener(this); llHint.setOnClickListener(this);
tvStarEmpty.setTypeface(helper.getFontawesome()); tvStarEmpty.setTypeface(helper.getFontawesome());

@ -33,33 +33,32 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.ShowHabitActivity; import org.isoron.uhabits.ShowHabitActivity;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.dialogs.HistoryEditorDialog; import org.isoron.uhabits.dialogs.HistoryEditorDialog;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitDataView; import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitFrequencyView; import org.isoron.uhabits.views.HabitFrequencyView;
import org.isoron.uhabits.views.HabitHistoryView; import org.isoron.uhabits.views.HabitHistoryView;
import org.isoron.uhabits.views.HabitScoreView; import org.isoron.uhabits.views.HabitScoreView;
import org.isoron.uhabits.views.HabitStreakView; import org.isoron.uhabits.views.HabitStreakView;
import org.isoron.uhabits.views.RepetitionCountView;
import org.isoron.uhabits.views.RingView; import org.isoron.uhabits.views.RingView;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class ShowHabitFragment extends Fragment public class ShowHabitFragment extends Fragment
implements DialogHelper.OnSavedListener, HistoryEditorDialog.Listener, implements UIHelper.OnSavedListener, HistoryEditorDialog.Listener,
Spinner.OnItemSelectedListener Spinner.OnItemSelectedListener
{ {
@Nullable @Nullable
@ -77,6 +76,8 @@ public class ShowHabitFragment extends Fragment
@Nullable @Nullable
private SharedPreferences prefs; private SharedPreferences prefs;
private int previousScoreInterval;
@Override @Override
public void onStart() public void onStart()
{ {
@ -89,7 +90,7 @@ public class ShowHabitFragment extends Fragment
{ {
View view = inflater.inflate(R.layout.show_habit, container, false); View view = inflater.inflate(R.layout.show_habit, container, false);
activity = (ShowHabitActivity) getActivity(); activity = (ShowHabitActivity) getActivity();
habit = activity.habit; habit = activity.getHabit();
dataViews = new LinkedList<>(); dataViews = new LinkedList<>();
@ -101,22 +102,19 @@ public class ShowHabitFragment extends Fragment
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1); int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1);
if(defaultScoreInterval > 5 || defaultScoreInterval < 0) defaultScoreInterval = 1; if(defaultScoreInterval > 5 || defaultScoreInterval < 0) defaultScoreInterval = 1;
previousScoreInterval = defaultScoreInterval;
setScoreBucketSize(defaultScoreInterval); setScoreBucketSize(defaultScoreInterval);
sStrengthInterval.setSelection(defaultScoreInterval); sStrengthInterval.setSelection(defaultScoreInterval);
sStrengthInterval.setOnItemSelectedListener(this); sStrengthInterval.setOnItemSelectedListener(this);
dataViews.add((HabitStreakView) view.findViewById(R.id.streakView));
dataViews.add((HabitStreakView) view.findViewById(R.id.smallStreakView));
dataViews.add((HabitScoreView) view.findViewById(R.id.scoreView)); dataViews.add((HabitScoreView) view.findViewById(R.id.scoreView));
dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView)); dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView));
dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView)); dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView));
dataViews.add((HabitStreakView) view.findViewById(R.id.streakView));
LinearLayout llRepetition = (LinearLayout) view.findViewById(R.id.llRepetition);
for(int i = 0; i < llRepetition.getChildCount(); i++)
dataViews.add((RepetitionCountView) llRepetition.getChildAt(i));
updateHeaders(view); updateHeaders(view);
updateScoreRing(view);
for(HabitDataView dataView : dataViews) for(HabitDataView dataView : dataViews)
dataView.setHabit(habit); dataView.setHabit(habit);
@ -145,12 +143,21 @@ public class ShowHabitFragment extends Fragment
} }
setHasOptionsMenu(true); setHasOptionsMenu(true);
return view; return view;
} }
@Override
public void onResume()
{
super.onResume();
refreshData();
}
private void updateScoreRing(View view) private void updateScoreRing(View view)
{ {
if(habit == null) return; if(habit == null) return;
if(view == null) return;
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing); RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
scoreRing.setColor(habit.color); scoreRing.setColor(habit.color);
@ -172,7 +179,6 @@ public class ShowHabitFragment extends Fragment
updateColor(view, R.id.tvStrength); updateColor(view, R.id.tvStrength);
updateColor(view, R.id.tvStreaks); updateColor(view, R.id.tvStreaks);
updateColor(view, R.id.tvWeekdayFreq); updateColor(view, R.id.tvWeekdayFreq);
updateColor(view, R.id.tvCount);
} }
private void updateColor(View view, int viewId) private void updateColor(View view, int viewId)
@ -229,12 +235,31 @@ public class ShowHabitFragment extends Fragment
} }
public void refreshData() public void refreshData()
{
new BaseTask()
{
@Override
protected void doInBackground()
{ {
if(dataViews == null) return; if(dataViews == null) return;
updateScoreRing(getView()); updateScoreRing(getView());
int count = 0;
for(HabitDataView view : dataViews) for(HabitDataView view : dataViews)
{
view.refreshData(); view.refreshData();
onProgressUpdate(count++);
}
}
@Override
protected void onProgressUpdate(Integer... values)
{
if(dataViews == null) return;
dataViews.get(values[0]).postInvalidate();
}
}.execute();
} }
@Override @Override
@ -246,17 +271,18 @@ public class ShowHabitFragment extends Fragment
private void setScoreBucketSize(int position) private void setScoreBucketSize(int position)
{ {
if(scoreView == null) return;
int sizes[] = { 1, 7, 31, 92, 365 }; int sizes[] = { 1, 7, 31, 92, 365 };
int size = sizes[position]; int size = sizes[position];
if(scoreView != null)
{
scoreView.setBucketSize(size); scoreView.setBucketSize(size);
scoreView.refreshData(); if(position != previousScoreInterval) refreshData();
}
if(prefs != null) if(prefs != null)
prefs.edit().putInt("pref_score_view_interval", position).apply(); prefs.edit().putInt("pref_score_view_interval", position).apply();
previousScoreInterval = position;
} }
@Override @Override

@ -20,11 +20,13 @@
package org.isoron.uhabits.helpers; package org.isoron.uhabits.helpers;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import com.activeandroid.ActiveAndroid; import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache;
import com.activeandroid.Configuration; import com.activeandroid.Configuration;
import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.BuildConfig;
@ -163,4 +165,20 @@ public class DatabaseHelper
ActiveAndroid.initialize(dbConfig); ActiveAndroid.initialize(dbConfig);
} }
public static long longQuery(String query, String args[])
{
Cursor c = null;
try
{
c = Cache.openDatabase().rawQuery(query, args);
if (!c.moveToFirst()) return 0;
return c.getLong(0);
}
finally
{
if(c != null) c.close();
}
}
} }

@ -23,6 +23,9 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Build;
import android.os.Debug;
import android.os.Looper;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -33,7 +36,7 @@ import android.view.inputmethod.InputMethodManager;
import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
public abstract class DialogHelper public abstract class UIHelper
{ {
public static final String ISORON_NAMESPACE = "http://isoron.org/android"; public static final String ISORON_NAMESPACE = "http://isoron.org/android";
@ -110,4 +113,37 @@ public abstract class DialogHelper
DisplayMetrics metrics = resources.getDisplayMetrics(); DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
} }
/**
* Throws a runtime exception if called from the main thread. Useful to make sure that
* slow methods never accidentally slow the application down.
*
* @throws RuntimeException when run from main thread
*/
public static void throwIfMainThread() throws RuntimeException
{
Looper looper = Looper.myLooper();
if(looper == null) return;
if(looper == Looper.getMainLooper())
throw new RuntimeException("This method should never be called from the main thread");
}
public static void startTracing()
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
{
throw new UnsupportedOperationException();
}
else
{
Debug.startMethodTracingSampling("Android/data/org.isoron.uhabits/perf",
32 * 1024 * 1024, 100);
}
}
public static void stopTracing()
{
Debug.stopMethodTracing();
}
} }

@ -19,13 +19,9 @@
package org.isoron.uhabits.loaders; package org.isoron.uhabits.loaders;
import android.os.AsyncTask;
import android.os.Handler;
import android.view.View;
import android.widget.ProgressBar;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -37,9 +33,8 @@ public class HabitListLoader
void onLoadFinished(); void onLoadFinished();
} }
private AsyncTask<Void, Integer, Void> currentFetchTask; private BaseTask currentFetchTask;
private int checkmarkCount; private int checkmarkCount;
private ProgressBar progressBar;
private Listener listener; private Listener listener;
private Long lastLoadTimestamp; private Long lastLoadTimestamp;
@ -56,11 +51,6 @@ public class HabitListLoader
this.includeArchived = includeArchived; this.includeArchived = includeArchived;
} }
public void setProgressBar(ProgressBar progressBar)
{
this.progressBar = progressBar;
}
public void setCheckmarkCount(int checkmarkCount) public void setCheckmarkCount(int checkmarkCount)
{ {
this.checkmarkCount = checkmarkCount; this.checkmarkCount = checkmarkCount;
@ -98,7 +88,7 @@ public class HabitListLoader
{ {
if (currentFetchTask != null) currentFetchTask.cancel(true); if (currentFetchTask != null) currentFetchTask.cancel(true);
currentFetchTask = new AsyncTask<Void, Integer, Void>() currentFetchTask = new BaseTask()
{ {
public HashMap<Long, Habit> newHabits; public HashMap<Long, Habit> newHabits;
public HashMap<Long, int[]> newCheckmarks; public HashMap<Long, int[]> newCheckmarks;
@ -106,7 +96,7 @@ public class HabitListLoader
public List<Habit> newHabitList; public List<Habit> newHabitList;
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
newHabits = new HashMap<>(); newHabits = new HashMap<>();
newCheckmarks = new HashMap<>(); newCheckmarks = new HashMap<>();
@ -136,12 +126,12 @@ public class HabitListLoader
commit(); commit();
if(!updateScoresAndCheckmarks) return null; if(!updateScoresAndCheckmarks) return;
int current = 0; int current = 0;
for (Habit h : newHabitList) for (Habit h : newHabitList)
{ {
if (isCancelled()) return null; if (isCancelled()) return;
Long id = h.getId(); Long id = h.getId();
newScores.put(id, h.scores.getTodayValue()); newScores.put(id, h.scores.getTodayValue());
@ -149,8 +139,6 @@ public class HabitListLoader
publishProgress(current++, newHabits.size()); publishProgress(current++, newHabits.size());
} }
return null;
} }
private void commit() private void commit()
@ -161,26 +149,9 @@ public class HabitListLoader
habitsList = newHabitList; habitsList = newHabitList;
} }
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(false);
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override @Override
protected void onProgressUpdate(Integer... values) protected void onProgressUpdate(Integer... values)
{ {
if(progressBar != null)
{
progressBar.setMax(values[1]);
progressBar.setProgress(values[0]);
}
if(listener != null) listener.onLoadFinished(); if(listener != null) listener.onLoadFinished();
} }
@ -189,11 +160,12 @@ public class HabitListLoader
{ {
if (isCancelled()) return; if (isCancelled()) return;
if(progressBar != null) progressBar.setVisibility(View.INVISIBLE);
lastLoadTimestamp = DateHelper.getStartOfToday(); lastLoadTimestamp = DateHelper.getStartOfToday();
currentFetchTask = null; currentFetchTask = null;
if(listener != null) listener.onLoadFinished(); if(listener != null) listener.onLoadFinished();
super.onPostExecute(null);
} }
}; };
@ -203,50 +175,29 @@ public class HabitListLoader
public void updateHabit(final Long id) public void updateHabit(final Long id)
{ {
new AsyncTask<Void, Void, Void>() new BaseTask()
{ {
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay; long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay;
Habit h = Habit.get(id); Habit h = Habit.get(id);
if(h == null) return;
habits.put(id, h); habits.put(id, h);
scores.put(id, h.scores.getTodayValue()); scores.put(id, h.scores.getTodayValue());
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
return null;
}
@Override
protected void onPreExecute()
{
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
if (getStatus() == Status.RUNNING)
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
}
}, 500);
} }
@Override @Override
protected void onPostExecute(Void aVoid) protected void onPostExecute(Void aVoid)
{ {
if(progressBar != null) progressBar.setVisibility(View.GONE);
if(listener != null) if(listener != null)
listener.onLoadFinished(); listener.onLoadFinished();
super.onPostExecute(null);
} }
}.execute(); }.execute();
} }

@ -21,15 +21,16 @@ package org.isoron.uhabits.models;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache; import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.UIHelper;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
@ -134,10 +135,9 @@ public class CheckmarkList
*/ */
protected void computeAll() protected void computeAll()
{ {
Repetition oldestRep = habit.repetitions.getOldest(); long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(oldestRep == null) return; if(fromTimestamp == 0) return;
Long fromTimestamp = oldestRep.timestamp;
Long toTimestamp = DateHelper.getStartOfToday(); Long toTimestamp = DateHelper.getStartOfToday();
compute(fromTimestamp, toTimestamp); compute(fromTimestamp, toTimestamp);
@ -150,9 +150,11 @@ public class CheckmarkList
* @param from timestamp for the beginning of the interval * @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval * @param to timestamp for the end of the interval
*/ */
protected void compute(long from, long to) protected void compute(long from, final long to)
{ {
long day = DateHelper.millisecondsInOneDay; UIHelper.throwIfMainThread();
final long day = DateHelper.millisecondsInOneDay;
Checkmark newestCheckmark = findNewest(); Checkmark newestCheckmark = findNewest();
if(newestCheckmark != null) if(newestCheckmark != null)
@ -165,9 +167,9 @@ public class CheckmarkList
.selectFromTo(fromExtended, to) .selectFromTo(fromExtended, to)
.execute(); .execute();
int nDays = (int) ((to - from) / day) + 1; final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1; int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
int checks[] = new int[nDaysExtended]; final int checks[] = new int[nDaysExtended];
for (Repetition rep : reps) for (Repetition rep : reps)
{ {
@ -187,24 +189,38 @@ public class CheckmarkList
checks[i] = Checkmark.CHECKED_IMPLICITLY; checks[i] = Checkmark.CHECKED_IMPLICITLY;
} }
ActiveAndroid.beginTransaction();
long timestamps[] = new long[nDays];
for (int i = 0; i < nDays; i++)
timestamps[i] = to - i * day;
insert(timestamps, checks);
}
private void insert(long timestamps[], int values[])
{
String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try try
{ {
for (int i = 0; i < nDays; i++) SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{ {
Checkmark c = new Checkmark(); statement.bindLong(1, habit.getId());
c.habit = habit; statement.bindLong(2, timestamps[i]);
c.timestamp = to - i * day; statement.bindLong(3, values[i]);
c.value = checks[i]; statement.execute();
c.save();
} }
ActiveAndroid.setTransactionSuccessful(); db.setTransactionSuccessful();
} }
finally finally
{ {
ActiveAndroid.endTransaction(); db.endTransaction();
} }
} }

@ -28,7 +28,9 @@ import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.From; import com.activeandroid.query.From;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.util.Arrays; import java.util.Arrays;
@ -96,22 +98,21 @@ public class RepetitionList
timestamp = DateHelper.getStartOfDay(timestamp); timestamp = DateHelper.getStartOfDay(timestamp);
if (contains(timestamp)) if (contains(timestamp))
{
delete(timestamp); delete(timestamp);
}
else else
{ insert(timestamp);
Repetition rep = new Repetition();
rep.habit = habit;
rep.timestamp = timestamp;
rep.save();
}
habit.scores.invalidateNewerThan(timestamp); habit.scores.invalidateNewerThan(timestamp);
habit.checkmarks.deleteNewerThan(timestamp); habit.checkmarks.deleteNewerThan(timestamp);
habit.streaks.deleteNewerThan(timestamp); habit.streaks.deleteNewerThan(timestamp);
} }
private void insert(long timestamp)
{
String[] args = { habit.getId().toString(), Long.toString(timestamp) };
SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args);
}
/** /**
* Returns the oldest repetition for the habit. If there is no repetition, returns null. * Returns the oldest repetition for the habit. If there is no repetition, returns null.
* Repetitions in the future are discarded. * Repetitions in the future are discarded.
@ -124,6 +125,21 @@ public class RepetitionList
return (Repetition) select().limit(1).executeSingle(); return (Repetition) select().limit(1).executeSingle();
} }
/**
* Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero.
* Repetitions in the future are discarded.
*
* @return timestamp of the oldest repetition
*/
public long getOldestTimestamp()
{
String[] args = { habit.getId().toString(), Long.toString(DateHelper.getStartOfToday()) };
String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " +
"order by timestamp limit 1";
return DatabaseHelper.longQuery(query, args);
}
/** /**
* Returns the total number of repetitions for each month, from the first repetition until * Returns the total number of repetitions for each month, from the first repetition until
* today, grouped by day of week. The repetitions are returned in a HashMap. The key is the * today, grouped by day of week. The repetitions are returned in a HashMap. The key is the

@ -21,6 +21,7 @@ package org.isoron.uhabits.models;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -28,9 +29,11 @@ import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.From; import com.activeandroid.query.From;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.UIHelper;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
@ -60,31 +63,6 @@ public class ScoreList
.orderBy("timestamp desc"); .orderBy("timestamp desc");
} }
/**
* Returns the most recent score already computed. If no score has been computed yet, returns
* null.
*
* @return newest score, or null if none exist
*/
@Nullable
protected Score findNewest()
{
return select().limit(1).executeSingle();
}
/**
* Returns the value of the most recent score that was already computed. If no score has been
* computed yet, returns zero.
*
* @return value of newest score, or zero if none exist
*/
protected int findNewestValue()
{
Score newest = findNewest();
if(newest == null) return 0;
else return newest.score;
}
/** /**
* Marks all scores that have timestamp equal to or newer than the given timestamp as invalid. * Marks all scores that have timestamp equal to or newer than the given timestamp as invalid.
* Any following getValue calls will trigger the scores to be recomputed. * Any following getValue calls will trigger the scores to be recomputed.
@ -104,10 +82,9 @@ public class ScoreList
*/ */
private void computeAll() private void computeAll()
{ {
Repetition oldestRep = habit.repetitions.getOldest(); long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(oldestRep == null) return; if(fromTimestamp == 0) return;
long fromTimestamp = oldestRep.timestamp;
long toTimestamp = DateHelper.getStartOfToday(); long toTimestamp = DateHelper.getStartOfToday();
compute(fromTimestamp, toTimestamp); compute(fromTimestamp, toTimestamp);
} }
@ -126,38 +103,82 @@ public class ScoreList
*/ */
protected void compute(long from, long to) protected void compute(long from, long to)
{ {
UIHelper.throwIfMainThread();
final long day = DateHelper.millisecondsInOneDay; final long day = DateHelper.millisecondsInOneDay;
final double freq = ((double) habit.freqNum) / habit.freqDen; final double freq = ((double) habit.freqNum) / habit.freqDen;
int newestScoreValue = findNewestValue(); int newestScoreValue = findNewestValue();
Score newestScore = findNewest(); long newestTimestamp = findNewestTimestamp();
if(newestScore != null) if(newestTimestamp > 0)
from = newestScore.timestamp + day; from = newestTimestamp + day;
final int checkmarkValues[] = habit.checkmarks.getValues(from, to); final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
final int firstScore = newestScoreValue;
final long beginning = from; final long beginning = from;
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() int lastScore = newestScoreValue;
{ int size = checkmarkValues.length;
@Override
public void execute() long timestamps[] = new long[size];
{ long values[] = new long[size];
int lastScore = firstScore;
for (int i = 0; i < checkmarkValues.length; i++) for (int i = 0; i < checkmarkValues.length; i++)
{ {
int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
lastScore = Score.compute(freq, lastScore, checkmarkValue);
timestamps[i] = beginning + day * i;
values[i] = lastScore;
}
insert(timestamps, values);
}
Score s = new Score(); /**
s.habit = habit; * Returns the value of the most recent score that was already computed. If no score has been
s.timestamp = beginning + day * i; * computed yet, returns zero.
s.score = lastScore = Score.compute(freq, lastScore, checkmarkValue); *
s.save(); * @return value of newest score, or zero if none exist
*/
protected int findNewestValue()
{
String args[] = { habit.getId().toString() };
String query = "select score from Score where habit = ? order by timestamp desc limit 1";
return SQLiteUtils.intQuery(query, args);
} }
private long findNewestTimestamp()
{
String args[] = { habit.getId().toString() };
String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1";
return DatabaseHelper.longQuery(query, args);
}
private void insert(long timestamps[], long values[])
{
String query = "insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
} }
});
} }
/** /**
@ -185,9 +206,9 @@ public class ScoreList
*/ */
public int getValue(long timestamp) public int getValue(long timestamp)
{ {
Score s = get(timestamp); computeAll();
if(s == null) return 0; String[] args = { habit.getId().toString(), Long.toString(timestamp) };
else return s.score; return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args);
} }
/** /**

@ -28,6 +28,7 @@ import com.activeandroid.query.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.UIHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
@ -86,6 +87,8 @@ public class StreakList
public void rebuild() public void rebuild()
{ {
UIHelper.throwIfMainThread();
long beginning; long beginning;
long today = DateHelper.getStartOfToday(); long today = DateHelper.getStartOfToday();
long day = DateHelper.millisecondsInOneDay; long day = DateHelper.millisecondsInOneDay;

@ -20,42 +20,53 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
public class BaseTask extends AsyncTask<Void, Void, Void> public abstract class BaseTask extends AsyncTask<Void, Integer, Void>
{ {
private static CountDownLatch latch;
private static int activeTaskCount; private static int activeTaskCount;
@Override
protected Void doInBackground(Void... params)
{
return null;
}
@Override @Override
protected void onPreExecute() protected void onPreExecute()
{ {
super.onPreExecute();
activeTaskCount++; activeTaskCount++;
latch = new CountDownLatch(activeTaskCount);
} }
@Override @Override
protected void onPostExecute(Void aVoid) protected void onPostExecute(Void aVoid)
{ {
activeTaskCount--; activeTaskCount--;
latch.countDown(); super.onPostExecute(null);
}
@Override
protected final Void doInBackground(Void... params)
{
doInBackground();
return null;
} }
public static void waitForTasks(long timeout, TimeUnit unit) protected abstract void doInBackground();
public static void waitForTasks(long timeout)
throws TimeoutException, InterruptedException throws TimeoutException, InterruptedException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
throw new UnsupportedOperationException("waitForTasks requires API 16+");
int poolInterval = 100;
while(timeout > 0)
{ {
if(activeTaskCount == 0) return; if(activeTaskCount == 0) return;
boolean successful = latch.await(timeout, unit); timeout -= poolInterval;
if(!successful) throw new TimeoutException(); Thread.sleep(poolInterval);
}
throw new TimeoutException();
} }
} }

@ -19,7 +19,6 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.os.AsyncTask;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.View; import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
@ -80,12 +79,12 @@ public class ExportCSVTask extends BaseTask
} }
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
try try
{ {
File dir = DatabaseHelper.getFilesDir("CSV"); File dir = DatabaseHelper.getFilesDir("CSV");
if(dir == null) return null; if(dir == null) return;
HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir); HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir);
archiveFilename = exporter.writeArchive(); archiveFilename = exporter.writeArchive();
@ -94,7 +93,5 @@ public class ExportCSVTask extends BaseTask
{ {
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
} }

@ -19,7 +19,6 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.os.AsyncTask;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.View; import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
@ -75,14 +74,14 @@ public class ExportDBTask extends BaseTask
} }
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
filename = null; filename = null;
try try
{ {
File dir = DatabaseHelper.getFilesDir("Backups"); File dir = DatabaseHelper.getFilesDir("Backups");
if(dir == null) return null; if(dir == null) return;
filename = DatabaseHelper.saveDatabaseCopy(dir); filename = DatabaseHelper.saveDatabaseCopy(dir);
} }
@ -90,7 +89,5 @@ public class ExportDBTask extends BaseTask
{ {
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
} }

@ -19,7 +19,6 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.View; import android.view.View;
@ -86,7 +85,7 @@ public class ImportDataTask extends BaseTask
} }
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
try try
{ {
@ -106,7 +105,5 @@ public class ImportDataTask extends BaseTask
result = FAILED; result = FAILED;
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
} }

@ -38,10 +38,9 @@ public class ToggleRepetitionTask extends BaseTask
} }
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
habit.repetitions.toggle(timestamp); habit.repetitions.toggle(timestamp);
return null;
} }
@Override @Override

@ -30,20 +30,18 @@ import android.text.Layout;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.View; import android.view.View;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
public class CheckmarkView extends View public class CheckmarkView extends View implements HabitDataView
{ {
private Paint pCard; private Paint pCard;
private Paint pIcon; private Paint pIcon;
private int primaryColor; private int primaryColor;
private int backgroundColor;
private int timesColor; private int timesColor;
private int darkGrey; private int darkGrey;
@ -56,12 +54,8 @@ public class CheckmarkView extends View
private String fa_check; private String fa_check;
private String fa_times; private String fa_times;
private String fa_full_star;
private String fa_half_star;
private String fa_empty_star;
private int check_status; private int check_status;
private int star_status;
private Rect rect; private Rect rect;
private TextPaint textPaint; private TextPaint textPaint;
@ -99,25 +93,19 @@ public class CheckmarkView extends View
fa_check = context.getString(R.string.fa_check); fa_check = context.getString(R.string.fa_check);
fa_times = context.getString(R.string.fa_times); fa_times = context.getString(R.string.fa_times);
fa_empty_star = context.getString(R.string.fa_star_o);
fa_half_star = context.getString(R.string.fa_star_half_o);
fa_full_star = context.getString(R.string.fa_star);
primaryColor = ColorHelper.palette[10]; primaryColor = ColorHelper.palette[10];
backgroundColor = Color.argb(255, 255, 255, 255);
timesColor = Color.argb(128, 255, 255, 255); timesColor = Color.argb(128, 255, 255, 255);
darkGrey = Color.argb(64, 0, 0, 0); darkGrey = Color.argb(64, 0, 0, 0);
rect = new Rect(); rect = new Rect();
check_status = 2; check_status = 0;
star_status = 0; label = "Habit";
label = "Wake up early";
} }
public void setHabit(Habit habit) public void setHabit(Habit habit)
{ {
this.habit = habit; this.habit = habit;
refreshData();
} }
@Override @Override
@ -190,21 +178,25 @@ public class CheckmarkView extends View
padding = 8 * leftMargin; padding = 8 * leftMargin;
textPaint.setTextSize(0.15f * width); textPaint.setTextSize(0.15f * width);
refreshData(); updateLabel();
} }
public void refreshData() public void refreshData()
{ {
this.check_status = habit.checkmarks.getTodayValue(); this.check_status = habit.checkmarks.getTodayValue();
this.star_status = habit.scores.getTodayStarStatus();
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color),
Color.blue(habit.color)); Color.blue(habit.color));
this.label = habit.name; this.label = habit.name;
updateLabel();
postInvalidate();
}
private void updateLabel()
{
textPaint.setColor(Color.WHITE); textPaint.setColor(Color.WHITE);
labelLayout = new StaticLayout(label, textPaint, labelLayout = new StaticLayout(label, textPaint,
(int) (width - 2 * leftMargin - 2 * padding), (int) (width - 2 * leftMargin - 2 * padding),
Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
} }
} }

@ -26,4 +26,6 @@ public interface HabitDataView
void setHabit(Habit habit); void setHabit(Habit habit);
void refreshData(); void refreshData();
void postInvalidate();
} }

@ -84,13 +84,10 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
{ {
this.habit = habit; this.habit = habit;
createColors(); createColors();
refreshData();
postInvalidate();
} }
private void init() private void init()
{ {
refreshData();
createPaints(); createPaints();
createColors(); createColors();
@ -181,7 +178,7 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV
else if(habit != null) else if(habit != null)
frequency = habit.repetitions.getWeekdayFrequency(); frequency = habit.repetitions.getWeekdayFrequency();
invalidate(); postInvalidate();
} }
private void generateRandomData() private void generateRandomData()

@ -26,15 +26,15 @@ import android.graphics.Paint;
import android.graphics.Paint.Align; import android.graphics.Paint.Align;
import android.graphics.RectF; import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.tasks.ToggleRepetitionTask; import org.isoron.uhabits.tasks.ToggleRepetitionTask;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -89,13 +89,10 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
{ {
this.habit = habit; this.habit = habit;
createColors(); createColors();
refreshData();
postInvalidate();
} }
private void init() private void init()
{ {
refreshData();
createPaints(); createPaints();
createColors(); createColors();
@ -136,7 +133,7 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
float baseSize = height / 8.0f; float baseSize = height / 8.0f;
setScrollerBucketSize((int) baseSize); setScrollerBucketSize((int) baseSize);
squareSpacing = DialogHelper.dpToPixels(getContext(), 1.0f); squareSpacing = UIHelper.dpToPixels(getContext(), 1.0f);
float maxTextSize = getResources().getDimensionPixelSize(R.dimen.regularTextSize); float maxTextSize = getResources().getDimensionPixelSize(R.dimen.regularTextSize);
float textSize = Math.min(baseSize * 0.5f, maxTextSize); float textSize = Math.min(baseSize * 0.5f, maxTextSize);
@ -221,7 +218,7 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
} }
updateDate(); updateDate();
invalidate(); postInvalidate();
} }
private void generateRandomData() private void generateRandomData()
@ -388,11 +385,23 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie
this.isEditable = isEditable; this.isEditable = isEditable;
} }
@Override @Override
public void onToggleRepetitionFinished() public void onToggleRepetitionFinished()
{
new BaseTask()
{
@Override
protected void doInBackground()
{ {
refreshData(); refreshData();
}
@Override
protected void onPostExecute(Void aVoid)
{
invalidate(); invalidate();
super.onPostExecute(null);
}
}.execute();
} }
} }

@ -93,15 +93,12 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
{ {
this.habit = habit; this.habit = habit;
createColors(); createColors();
refreshData();
postInvalidate();
} }
private void init() private void init()
{ {
createPaints(); createPaints();
createColors(); createColors();
if(isInEditMode()) refreshData();
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault()); dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
@ -186,7 +183,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
scores = habit.scores.getAllValues(bucketSize); scores = habit.scores.getAllValues(bucketSize);
} }
invalidate(); postInvalidate();
} }
public void setBucketSize(int bucketSize) public void setBucketSize(int bucketSize)

@ -78,9 +78,7 @@ public class HabitStreakView extends View implements HabitDataView
public void setHabit(Habit habit) public void setHabit(Habit habit)
{ {
this.habit = habit; this.habit = habit;
createColors(); createColors();
postInvalidate();
} }
private void init() private void init()
@ -93,7 +91,7 @@ public class HabitStreakView extends View implements HabitDataView
dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
rect = new RectF(); rect = new RectF();
maxStreakCount = 10;
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
} }
@ -118,7 +116,6 @@ public class HabitStreakView extends View implements HabitDataView
em = paint.getFontSpacing(); em = paint.getFontSpacing();
textMargin = 0.5f * em; textMargin = 0.5f * em;
refreshData();
updateMaxMin(); updateMaxMin();
} }

@ -33,7 +33,7 @@ import android.view.View;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
public class NumberView extends View public class NumberView extends View
{ {
@ -66,9 +66,9 @@ public class NumberView extends View
this.textSize = getResources().getDimension(R.dimen.regularTextSize); this.textSize = getResources().getDimension(R.dimen.regularTextSize);
this.label = DialogHelper.getAttribute(context, attrs, "label", "Number"); this.label = UIHelper.getAttribute(context, attrs, "label", "Number");
this.number = DialogHelper.getIntAttribute(context, attrs, "number", 0); this.number = UIHelper.getIntAttribute(context, attrs, "number", 0);
this.textSize = DialogHelper.getFloatAttribute(context, attrs, "textSize", this.textSize = UIHelper.getFloatAttribute(context, attrs, "textSize",
getResources().getDimension(R.dimen.regularTextSize)); getResources().getDimension(R.dimen.regularTextSize));
this.color = ColorHelper.palette[7]; this.color = ColorHelper.palette[7];
@ -92,7 +92,6 @@ public class NumberView extends View
public void setNumber(int number) public void setNumber(int number)
{ {
this.number = number; this.number = number;
requestLayout();
postInvalidate(); postInvalidate();
} }

@ -24,7 +24,7 @@ import android.util.AttributeSet;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.Calendar; import java.util.Calendar;
@ -38,14 +38,12 @@ public class RepetitionCountView extends NumberView implements HabitDataView
public RepetitionCountView(Context context, AttributeSet attrs) public RepetitionCountView(Context context, AttributeSet attrs)
{ {
super(context, attrs); super(context, attrs);
this.interval = DialogHelper.getIntAttribute(context, attrs, "interval", 7); this.interval = UIHelper.getIntAttribute(context, attrs, "interval", 7);
int labelValue = DialogHelper.getIntAttribute(context, attrs, "labelValue", 7); int labelValue = UIHelper.getIntAttribute(context, attrs, "labelValue", 7);
String labelFormat = DialogHelper.getAttribute(context, attrs, "labelFormat", String labelFormat = UIHelper.getAttribute(context, attrs, "labelFormat",
getResources().getString(R.string.last_x_days)); getResources().getString(R.string.last_x_days));
setLabel(String.format(labelFormat, labelValue)); setLabel(String.format(labelFormat, labelValue));
refreshData();
} }
@Override @Override
@ -73,6 +71,8 @@ public class RepetitionCountView extends NumberView implements HabitDataView
if(habit != null) if(habit != null)
setNumber(habit.repetitions.count(from, to)); setNumber(habit.repetitions.count(from, to));
postInvalidate();
} }
@Override @Override
@ -80,6 +80,5 @@ public class RepetitionCountView extends NumberView implements HabitDataView
{ {
this.habit = habit; this.habit = habit;
setColor(habit.color); setColor(habit.color);
refreshData();
} }
} }

@ -29,11 +29,10 @@ import android.text.Layout;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.View; import android.view.View;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
public class RingView extends View public class RingView extends View
{ {
@ -63,9 +62,9 @@ public class RingView extends View
{ {
super(context, attrs); super(context, attrs);
this.label = DialogHelper.getAttribute(context, attrs, "label", "Label"); this.label = UIHelper.getAttribute(context, attrs, "label", "Label");
this.maxDiameter = DialogHelper.getFloatAttribute(context, attrs, "maxDiameter", 100); this.maxDiameter = UIHelper.getFloatAttribute(context, attrs, "maxDiameter", 100);
this.maxDiameter = DialogHelper.dpToPixels(context, maxDiameter); this.maxDiameter = UIHelper.dpToPixels(context, maxDiameter);
init(); init();
} }
@ -98,7 +97,6 @@ public class RingView extends View
fadedTextColor = getResources().getColor(R.color.fadedTextColor); fadedTextColor = getResources().getColor(R.color.fadedTextColor);
textSize = getResources().getDimension(R.dimen.smallTextSize); textSize = getResources().getDimension(R.dimen.smallTextSize);
Log.d("RingView", String.format("textSize=%f", textSize));
rect = new RectF(); rect = new RectF();
} }

@ -34,9 +34,10 @@ import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -92,12 +93,12 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
} }
} }
private void updateWidget(Context context, AppWidgetManager manager, int widgetId, Bundle options) private void updateWidget(Context context, AppWidgetManager manager,
int widgetId, Bundle options)
{ {
updateWidgetSize(context, options); updateWidgetSize(context, options);
Context appContext = context.getApplicationContext(); Context appContext = context.getApplicationContext();
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), getLayoutId());
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L); Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L);
@ -112,24 +113,11 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
return; return;
} }
View widgetView = buildCustomView(context, habit); new RenderWidgetTask(widgetId, context, habit, manager).execute();
measureCustomView(context, width, height, widgetView);
widgetView.setDrawingCacheEnabled(true);
widgetView.buildDrawingCache(true);
Bitmap drawingCache = widgetView.getDrawingCache();
remoteViews.setTextViewText(R.id.label, habit.name);
remoteViews.setImageViewBitmap(R.id.imageView, drawingCache);
//savePreview(context, widgetId, drawingCache);
PendingIntent onClickIntent = getOnClickPendingIntent(context, habit);
if(onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.imageView, onClickIntent);
manager.updateAppWidget(widgetId, remoteViews);
} }
protected abstract void refreshCustomViewData(View widgetView);
private void savePreview(Context context, int widgetId, Bitmap widgetCache) private void savePreview(Context context, int widgetId, Bitmap widgetCache)
{ {
try try
@ -170,13 +158,13 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
{ {
maxWidth = (int) DialogHelper.dpToPixels(context, maxWidth = (int) UIHelper.dpToPixels(context,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH));
maxHeight = (int) DialogHelper.dpToPixels(context, maxHeight = (int) UIHelper.dpToPixels(context,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)); options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT));
minWidth = (int) DialogHelper.dpToPixels(context, minWidth = (int) UIHelper.dpToPixels(context,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)); options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH));
minHeight = (int) DialogHelper.dpToPixels(context, minHeight = (int) UIHelper.dpToPixels(context,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)); options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT));
} }
@ -204,4 +192,60 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
customView.measure(specWidth, specHeight); customView.measure(specWidth, specHeight);
customView.layout(0, 0, customView.getMeasuredWidth(), customView.getMeasuredHeight()); customView.layout(0, 0, customView.getMeasuredWidth(), customView.getMeasuredHeight());
} }
private class RenderWidgetTask extends BaseTask
{
private final int widgetId;
private final Context context;
private final Habit habit;
private final AppWidgetManager manager;
public RemoteViews remoteViews;
public View widgetView;
public RenderWidgetTask(int widgetId, Context context, Habit habit,
AppWidgetManager manager)
{
this.widgetId = widgetId;
this.context = context;
this.habit = habit;
this.manager = manager;
}
@Override
protected void onPreExecute()
{
super.onPreExecute();
remoteViews = new RemoteViews(context.getPackageName(), getLayoutId());
widgetView = buildCustomView(context, habit);
measureCustomView(context, width, height, widgetView);
manager.updateAppWidget(widgetId, remoteViews);
}
@Override
protected void doInBackground()
{
refreshCustomViewData(widgetView);
}
@Override
protected void onPostExecute(Void aVoid)
{
widgetView.invalidate();
widgetView.setDrawingCacheEnabled(true);
widgetView.buildDrawingCache(true);
Bitmap drawingCache = widgetView.getDrawingCache();
remoteViews.setTextViewText(R.id.label, habit.name);
remoteViews.setImageViewBitmap(R.id.imageView, drawingCache);
//savePreview(context, widgetId, drawingCache);
PendingIntent onClickIntent = getOnClickPendingIntent(context, habit);
if(onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.imageView, onClickIntent);
manager.updateAppWidget(widgetId, remoteViews);
super.onPostExecute(aVoid);
}
}
} }

@ -26,6 +26,7 @@ import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.CheckmarkView; import org.isoron.uhabits.views.CheckmarkView;
import org.isoron.uhabits.views.HabitDataView;
public class CheckmarkWidgetProvider extends BaseWidgetProvider public class CheckmarkWidgetProvider extends BaseWidgetProvider
{ {
@ -37,6 +38,12 @@ public class CheckmarkWidgetProvider extends BaseWidgetProvider
return view; return view;
} }
@Override
protected void refreshCustomViewData(View view)
{
((HabitDataView) view).refreshData();
}
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
{ {
@ -60,4 +67,6 @@ public class CheckmarkWidgetProvider extends BaseWidgetProvider
{ {
return R.layout.widget_checkmark; return R.layout.widget_checkmark;
} }
} }

@ -26,6 +26,7 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitFrequencyView; import org.isoron.uhabits.views.HabitFrequencyView;
public class FrequencyWidgetProvider extends BaseWidgetProvider public class FrequencyWidgetProvider extends BaseWidgetProvider
@ -39,6 +40,12 @@ public class FrequencyWidgetProvider extends BaseWidgetProvider
return view; return view;
} }
@Override
protected void refreshCustomViewData(View view)
{
((HabitDataView) view).refreshData();
}
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
{ {

@ -25,6 +25,7 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitHistoryView; import org.isoron.uhabits.views.HabitHistoryView;
public class HistoryWidgetProvider extends BaseWidgetProvider public class HistoryWidgetProvider extends BaseWidgetProvider
@ -38,6 +39,12 @@ public class HistoryWidgetProvider extends BaseWidgetProvider
return view; return view;
} }
@Override
protected void refreshCustomViewData(View view)
{
((HabitDataView) view).refreshData();
}
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
{ {

@ -25,6 +25,7 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitScoreView; import org.isoron.uhabits.views.HabitScoreView;
public class ScoreWidgetProvider extends BaseWidgetProvider public class ScoreWidgetProvider extends BaseWidgetProvider
@ -38,6 +39,12 @@ public class ScoreWidgetProvider extends BaseWidgetProvider
return view; return view;
} }
@Override
protected void refreshCustomViewData(View view)
{
((HabitDataView) view).refreshData();
}
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
{ {

@ -25,6 +25,7 @@ import android.view.View;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.views.HabitStreakView; import org.isoron.uhabits.views.HabitStreakView;
public class StreakWidgetProvider extends BaseWidgetProvider public class StreakWidgetProvider extends BaseWidgetProvider
@ -38,6 +39,12 @@ public class StreakWidgetProvider extends BaseWidgetProvider
return view; return view;
} }
@Override
protected void refreshCustomViewData(View view)
{
((HabitDataView) view).refreshData();
}
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 34 KiB

@ -53,75 +53,6 @@
app:maxDiameter="80" app:maxDiameter="80"
app:textSize="@dimen/smallTextSize"/> app:textSize="@dimen/smallTextSize"/>
<LinearLayout
style="@style/smallDataViewStyle"
android:orientation="vertical">
<org.isoron.uhabits.views.HabitStreakView
android:id="@+id/smallStreakView"
android:layout_width="match_parent"
android:layout_height="80dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/best_streaks"
android:layout_marginTop="9dp"
android:textColor="@color/fadedTextColor"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
style="@style/cardStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top|start"
android:visibility="gone">
<TextView
android:id="@+id/tvCount"
style="@style/cardHeaderStyle"
android:text="@string/number_of_repetitions"/>
<LinearLayout
android:id="@+id/llRepetition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<org.isoron.uhabits.views.RepetitionCountView
style="@style/smallDataViewStyle"
app:interval="30"
app:labelFormat="@string/last_x_days"
app:labelValue="30"
app:textSize="12"/>
<org.isoron.uhabits.views.RepetitionCountView
style="@style/smallDataViewStyle"
app:interval="92"
app:labelFormat="@string/last_x_months"
app:labelValue="4"
app:textSize="12"/>
<org.isoron.uhabits.views.RepetitionCountView
style="@style/smallDataViewStyle"
app:interval="365"
app:labelFormat="@string/last_x_months"
app:labelValue="12"
app:textSize="12"/>
<org.isoron.uhabits.views.RepetitionCountView
style="@style/smallDataViewStyle"
app:interval="0"
app:label="@string/all_time"
app:textSize="12"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

Loading…
Cancel
Save