Major refactoring of ListHabitsActivity

pull/145/head
Alinson S. Xavier 9 years ago
parent 3ffa079e24
commit 7e8a2a0c1c

@ -1,5 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.getkeepsafe.dexcount'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 23
@ -14,7 +16,6 @@ android {
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//testInstrumentationRunnerArgument "size", "small"
}
buildTypes {
@ -30,6 +31,10 @@ android {
lintOptions {
checkReleaseBuilds false
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}
dependencies {
@ -41,11 +46,21 @@ dependencies {
compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7'
compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
compile 'com.jakewharton:butterknife:8.0.1'
apt 'com.jakewharton:butterknife-compiler:8.0.1'
compile 'com.google.dagger:dagger:2.2'
apt 'com.google.dagger:dagger-compiler:2.2'
testApt 'com.google.dagger:dagger-compiler:2.2'
androidTestApt 'com.google.dagger:dagger-compiler:2.2'
provided 'javax.annotation:jsr250-api:1.0'
compile project(':libs:drag-sort-listview:library')
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'com.android.support:support-annotations:23.3.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidTestComponent extends BaseComponent
{
void inject(BaseAndroidTest baseAndroidTest);
}

@ -27,11 +27,14 @@ import android.support.test.InstrumentationRegistry;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.Preferences;
import org.junit.Before;
import java.util.concurrent.TimeoutException;
public class BaseTest
import javax.inject.Inject;
public class BaseAndroidTest
{
protected Context testContext;
protected Context targetContext;
@ -39,8 +42,12 @@ public class BaseTest
public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC)
@Inject
protected Preferences prefs;
protected AndroidTestComponent androidTestComponent;
@Before
public void setup()
public void setUp()
{
if(!isLooperPrepared)
{
@ -53,6 +60,10 @@ public class BaseTest
InterfaceUtils.setFixedTheme(R.style.AppBaseTheme);
DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME);
androidTestComponent = DaggerAndroidTestComponent.builder().build();
HabitsApplication.setComponent(androidTestComponent);
androidTestComponent.inject(this);
}
protected void waitForAsyncTasks() throws InterruptedException, TimeoutException

@ -61,7 +61,7 @@ public class HabitViewActions
@Override
public void perform(UiController uiController, View view)
{
if (view.getId() != R.id.llButtons)
if (view.getId() != R.id.checkmarkPanel)
throw new InvalidParameterException("View must have id llButtons");
LinearLayout llButtons = (LinearLayout) view;

@ -191,7 +191,7 @@ public class MainTest
String name = addHabit(true);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.llButtons))
.onChildView(withId(R.id.checkmarkPanel))
.perform(toggleAllCheckmarks());
Thread.sleep(1200);

@ -23,8 +23,9 @@ import android.os.Build;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.ui.BaseSystem;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -35,7 +36,7 @@ import static org.hamcrest.Matchers.containsString;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsApplicationTest extends BaseTest
public class HabitsApplicationTest extends BaseAndroidTest
{
@Test
public void test_getLogcat() throws IOException
@ -49,7 +50,8 @@ public class HabitsApplicationTest extends BaseTest
HabitsApplication app = HabitsApplication.getInstance();
assert(app != null);
String log = app.getLogcat();
BaseSystem system = new BaseSystem(targetContext);
String log = system.getLogcat();
assertThat(log, containsString(msg));
}
}

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -37,16 +37,16 @@ import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ArchiveHabitsCommandTest extends BaseTest
public class ArchiveHabitsCommandTest extends BaseAndroidTest
{
private ArchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
habit = HabitFixtures.createShortHabit();
command = new ArchiveHabitsCommand(Collections.singletonList(habit));

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -37,15 +37,15 @@ import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ChangeHabitColorCommandTest extends BaseTest
public class ChangeHabitColorCommandTest extends BaseAndroidTest
{
private ChangeHabitColorCommand command;
private LinkedList<Habit> habits;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
habits = new LinkedList<>();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -38,16 +38,16 @@ import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CreateHabitCommandTest extends BaseTest
public class CreateHabitCommandTest extends BaseAndroidTest
{
private CreateHabitCommand command;
private Habit model;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
model = new Habit();
model.name = "New habit";

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -39,7 +39,7 @@ import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DeleteHabitsCommandTest extends BaseTest
public class DeleteHabitsCommandTest extends BaseAndroidTest
{
private DeleteHabitsCommand command;
private LinkedList<Habit> habits;
@ -48,9 +48,9 @@ public class DeleteHabitsCommandTest extends BaseTest
public ExpectedException thrown = ExpectedException.none();
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habits = new LinkedList<>();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -37,7 +37,7 @@ import static org.hamcrest.Matchers.greaterThan;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class EditHabitCommandTest extends BaseTest
public class EditHabitCommandTest extends BaseAndroidTest
{
private EditHabitCommand command;
@ -46,9 +46,9 @@ public class EditHabitCommandTest extends BaseTest
private Long id;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
habit = HabitFixtures.createShortHabit();
habit.name = "original";

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
@ -36,7 +36,7 @@ import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ToggleRepetitionCommandTest extends BaseTest
public class ToggleRepetitionCommandTest extends BaseAndroidTest
{
private ToggleRepetitionCommand command;
@ -44,9 +44,9 @@ public class ToggleRepetitionCommandTest extends BaseTest
private long today;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
habit = HabitFixtures.createShortHabit();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -37,16 +37,16 @@ import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UnarchiveHabitsCommandTest extends BaseTest
public class UnarchiveHabitsCommandTest extends BaseAndroidTest
{
private UnarchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
habit = HabitFixtures.createShortHabit();
Habit.archive(Collections.singletonList(habit));

@ -24,7 +24,7 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.io.HabitsCSVExporter;
import org.isoron.uhabits.models.Habit;
@ -45,14 +45,14 @@ import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsCSVExporterTest extends BaseTest
public class HabitsCSVExporterTest extends BaseAndroidTest
{
private File baseDir;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
HabitFixtures.createShortHabit();

@ -24,7 +24,7 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.io.GenericImporter;
@ -49,15 +49,15 @@ import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportTest extends BaseTest
public class ImportTest extends BaseAndroidTest
{
private File baseDir;
private Context context;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
DateUtils.setFixedLocalTime(null);
HabitFixtures.purgeHabits();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -42,15 +42,15 @@ import static org.isoron.uhabits.models.Checkmark.UNCHECKED;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkListTest extends BaseTest
public class CheckmarkListTest extends BaseAndroidTest
{
Habit nonDailyHabit;
private Habit emptyHabit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
nonDailyHabit = HabitFixtures.createShortHabit();

@ -23,7 +23,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.hamcrest.MatcherAssert;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
@ -45,12 +45,12 @@ import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitTest extends BaseTest
public class HabitTest extends BaseAndroidTest
{
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
}

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
@ -44,15 +44,15 @@ import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RepetitionListTest extends BaseTest
public class RepetitionListTest extends BaseAndroidTest
{
private Habit habit;
private Habit emptyHabit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createShortHabit();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.models.Habit;
@ -41,14 +41,14 @@ import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreListTest extends BaseTest
public class ScoreListTest extends BaseAndroidTest
{
private Habit habit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createEmptyHabit();

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Score;
import org.junit.Before;
@ -34,12 +34,12 @@ import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreTest extends BaseTest
public class ScoreTest extends BaseAndroidTest
{
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
}
@Test

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.tasks;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.unit.HabitFixtures;
@ -41,12 +41,12 @@ import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportCSVTaskTest extends BaseTest
public class ExportCSVTaskTest extends BaseAndroidTest
{
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
}
@Test

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.tasks;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.junit.Before;
import org.junit.Test;
@ -38,12 +38,12 @@ import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportDBTaskTest extends BaseTest
public class ExportDBTaskTest extends BaseAndroidTest
{
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
}
@Test

@ -23,7 +23,7 @@ import android.support.annotation.NonNull;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.tasks.ImportDataTask;
import org.isoron.uhabits.utils.FileUtils;
import org.junit.Before;
@ -40,14 +40,14 @@ import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportDataTaskTest extends BaseTest
public class ImportDataTaskTest extends BaseAndroidTest
{
private File baseDir;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
baseDir = FileUtils.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");

@ -0,0 +1,187 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.ui.habits.list.view;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkButtonView;
import org.isoron.uhabits.unit.views.ViewTest;
import org.isoron.uhabits.utils.ColorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkButtonViewTest extends ViewTest
{
public static final String PATH = "ui/habits/list/CheckmarkButtonView/";
private CountDownLatch latch;
private CheckmarkButtonView view;
@Before
public void setUp()
{
super.setUp();
setSimilarityCutoff(0.03f);
latch = new CountDownLatch(1);
view = new CheckmarkButtonView(targetContext);
view.setValue(Checkmark.UNCHECKED);
view.setColor(ColorUtils.CSV_PALETTE[7]);
measureView(dpToPixels(40), dpToPixels(40), view);
}
protected void assertRendersCheckedExplicitly() throws IOException
{
assertRenders(view, PATH + "render_explicit_check.png");
}
protected void assertRendersUnchecked() throws IOException
{
assertRenders(view, PATH + "render_unchecked.png");
}
protected void assertRendersCheckedImplicitly() throws IOException
{
assertRenders(view, PATH + "render_implicit_check.png");
}
@Test
public void testRender_unchecked() throws Exception
{
view.setValue(Checkmark.UNCHECKED);
assertRendersUnchecked();
}
@Test
public void testRender_explicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_EXPLICITLY);
assertRendersCheckedExplicitly();
}
@Test
public void testRender_implicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_IMPLICITLY);
assertRendersCheckedImplicitly();
}
// @Test
// public void testLongClick() throws Exception
// {
// setOnToggleListener();
// view.performLongClick();
// waitForLatch();
// assertRendersCheckedExplicitly();
// }
//
// @Test
// public void testClick_withShortToggle_fromUnchecked() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.UNCHECKED);
// setOnToggleListenerAndPerformClick();
// assertRendersCheckedExplicitly();
// }
//
// @Test
// public void testClick_withShortToggle_fromChecked() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.CHECKED_EXPLICITLY);
// setOnToggleListenerAndPerformClick();
// assertRendersUnchecked();
// }
//
// @Test
// public void testClick_withShortToggle_withoutListener() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.CHECKED_EXPLICITLY);
// view.setController(null);
// view.performClick();
// assertRendersUnchecked();
// }
//
// protected void setOnToggleListenerAndPerformClick() throws InterruptedException
// {
// setOnToggleListener();
// view.performClick();
// waitForLatch();
// }
//
// @Test
// public void testClick_withoutShortToggle() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(false);
// setOnInvalidToggleListener();
// view.performClick();
// waitForLatch();
// assertRendersUnchecked();
// }
// protected void setOnInvalidToggleListener()
// {
// view.setController(new CheckmarkButtonView.Controller()
// {
// @Override
// public void onToggleCheckmark(CheckmarkButtonView view, long timestamp)
// {
// fail();
// }
//
// @Override
// public void onInvalidToggle(CheckmarkButtonView v)
// {
// assertThat(v, equalTo(view));
// latch.countDown();
// }
// });
// }
// protected void setOnToggleListener()
// {
// view.setController(new CheckmarkButtonView.Controller()
// {
// @Override
// public void onToggleCheckmark(CheckmarkButtonView v, long t)
// {
// assertThat(v, equalTo(view));
// assertThat(t, equalTo(DateUtils.getStartOfToday()));
// latch.countDown();
// }
//
// @Override
// public void onInvalidToggle(CheckmarkButtonView view)
// {
// fail();
// }
// });
// }
}

@ -0,0 +1,97 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.ui.habits.list.view;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkPanelView;
import org.isoron.uhabits.unit.views.ViewTest;
import org.isoron.uhabits.utils.ColorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkPanelViewTest extends ViewTest
{
public static final String PATH = "ui/habits/list/CheckmarkPanelView/";
private CountDownLatch latch;
private CheckmarkPanelView view;
private int checkmarks[];
@Override
@Before
public void setUp()
{
super.setUp();
setSimilarityCutoff(0.03f);
prefs.setShouldReverseCheckmarks(false);
Habit habit = new Habit();
latch = new CountDownLatch(1);
checkmarks = new int[]{Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED,
Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY};
view = new CheckmarkPanelView(targetContext);
view.setHabit(habit);
view.setCheckmarkValues(checkmarks);
view.setColor(ColorUtils.CSV_PALETTE[7]);
measureView(dpToPixels(200), dpToPixels(200), view);
}
// protected void waitForLatch() throws InterruptedException
// {
// assertTrue("Latch timeout", latch.await(1, TimeUnit.SECONDS));
// }
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
// @Test
// public void testToggleCheckmark_withLeftToRight() throws Exception
// {
// setToggleListener();
// view.getButton(1).performToggle();
// waitForLatch();
// }
//
// @Test
// public void testToggleCheckmark_withReverseCheckmarks() throws Exception
// {
// prefs.setShouldReverseCheckmarks(true);
// view.setCheckmarkValues(checkmarks); // refresh after preference change
//
// setToggleListener();
// view.getButton(2).performToggle();
// waitForLatch();
// }
}

@ -42,9 +42,9 @@ public class CheckmarkWidgetViewTest extends ViewTest
private Habit habit;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme);
habit = HabitFixtures.createShortHabit();

@ -36,9 +36,9 @@ public class HabitFrequencyViewTest extends ViewTest
private HabitFrequencyView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();

@ -43,9 +43,9 @@ public class HabitHistoryViewTest extends ViewTest
private HabitHistoryView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();

@ -38,9 +38,9 @@ public class HabitScoreViewTest extends ViewTest
private HabitScoreView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();

@ -36,9 +36,9 @@ public class HabitStreakViewTest extends ViewTest
private HabitStreakView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();

@ -38,9 +38,9 @@ public class NumberViewTest extends ViewTest
private NumberView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
view = new NumberView(targetContext);
view.setLabel("Hello world");

@ -38,9 +38,9 @@ public class RingViewTest extends ViewTest
private RingView view;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
view = new RingView(targetContext);
view.setPercentage(0.6f);

@ -26,7 +26,7 @@ import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.tasks.BaseTask;
@ -39,10 +39,23 @@ import java.io.InputStream;
import static junit.framework.Assert.fail;
public class ViewTest extends BaseTest
public class ViewTest extends BaseAndroidTest
{
protected static final double SIMILARITY_CUTOFF = 0.09;
protected static final double DEFAULT_SIMILARITY_CUTOFF = 0.09;
public static final int HISTOGRAM_BIN_SIZE = 8;
private double similarityCutoff;
@Override
public void setUp()
{
super.setUp();
similarityCutoff = DEFAULT_SIMILARITY_CUTOFF;
}
protected void setSimilarityCutoff(double similarityCutoff)
{
this.similarityCutoff = similarityCutoff;
}
protected void measureView(int width, int height, View view)
{
@ -70,7 +83,8 @@ public class ViewTest extends BaseTest
double distance;
boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) >
similarityCutoff)
{
similarEnough = false;
errorMessage.append(String.format(

@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidComponent extends BaseComponent
{
}

@ -0,0 +1,54 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.utils.Preferences;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class AndroidModule
{
@Provides
@Singleton
Preferences providePreferences()
{
return new Preferences();
}
@Provides
@Singleton
CommandRunner provideCommandRunner()
{
return new CommandRunner();
}
@Provides
@Singleton
HabitCardListCache provideHabitCardListCache()
{
return new HabitCardListCache();
}
}

@ -0,0 +1,53 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import org.isoron.uhabits.tasks.ToggleRepetitionTask;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.list.ListHabitsSelectionMenu;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.ui.habits.list.ListHabitsController;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.model.HintList;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkPanelView;
public interface BaseComponent
{
void inject(CheckmarkButtonController checkmarkButtonController);
void inject(ListHabitsController listHabitsController);
void inject(CheckmarkPanelView checkmarkPanelView);
void inject(ToggleRepetitionTask toggleRepetitionTask);
void inject(BaseDialogFragment baseDialogFragment);
void inject(HabitCardListCache habitCardListCache);
void inject(HabitBroadcastReceiver habitBroadcastReceiver);
void inject(ListHabitsSelectionMenu listHabitsSelectionMenu);
void inject(HintList hintList);
void inject(HabitCardListAdapter habitCardListAdapter);
}

@ -38,16 +38,18 @@ import android.support.v4.content.LocalBroadcastManager;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.widgets.WidgetManager;
import java.util.Date;
import javax.inject.Inject;
public class HabitBroadcastReceiver extends BroadcastReceiver
{
public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
@ -55,6 +57,15 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
@Inject
CommandRunner commandRunner;
public HabitBroadcastReceiver()
{
super();
HabitsApplication.getComponent().inject(this);
}
@Override
public void onReceive(final Context context, Intent intent)
{
@ -85,14 +96,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
private void createReminderAlarmsDelayed(final Context context)
{
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
ReminderUtils.createReminderAlarms(context);
}
}, 5000);
new Handler().postDelayed(() -> ReminderUtils.createReminderAlarms(context), 5000);
}
private void snoozeHabit(Context context, Intent intent)
@ -119,7 +123,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
if(habit != null)
{
ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp);
CommandRunner.getInstance().execute(command, habitId);
commandRunner.execute(command, habitId);
}
dismissNotification(context, habitId);

@ -21,28 +21,15 @@ package org.isoron.uhabits;
import android.app.Application;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.WindowManager;
import com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.widgets.WidgetManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
public class HabitsApplication extends Application implements MainController.System
public class HabitsApplication extends Application
{
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_IMPORT_DATA = 1;
@ -55,6 +42,7 @@ public class HabitsApplication extends Application implements MainController.Sys
@Nullable
private static Context context;
private static BaseComponent component;
public static boolean isTestMode()
{
@ -82,12 +70,23 @@ public class HabitsApplication extends Application implements MainController.Sys
return application;
}
public static BaseComponent getComponent()
{
return component;
}
public static void setComponent(BaseComponent component)
{
HabitsApplication.component = component;
}
@Override
public void onCreate()
{
super.onCreate();
HabitsApplication.context = this;
HabitsApplication.application = this;
component = DaggerAndroidComponent.builder().build();
if (isTestMode())
{
@ -105,108 +104,4 @@ public class HabitsApplication extends Application implements MainController.Sys
ActiveAndroid.dispose();
super.onTerminate();
}
public String getLogcat() throws IOException
{
int maxNLines = 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<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if(log.size() > maxNLines) log.removeFirst();
}
for(String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
public String getDeviceInfo()
{
if(context == null) return "";
StringBuilder b = new StringBuilder();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
b.append(String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME));
b.append(String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE));
b.append(String.format("OS Version: %s (%s)\n", java.lang.System.getProperty("os.version"),
android.os.Build.VERSION.INCREMENTAL));
b.append(String.format("OS API Level: %s\n", android.os.Build.VERSION.SDK));
b.append(String.format("Device: %s\n", android.os.Build.DEVICE));
b.append(String.format("Model (Product): %s (%s)\n", android.os.Build.MODEL,
android.os.Build.PRODUCT));
b.append(String.format("Manufacturer: %s\n", android.os.Build.MANUFACTURER));
b.append(String.format("Other tags: %s\n", android.os.Build.TAGS));
b.append(String.format("Screen Width: %s\n", wm.getDefaultDisplay().getWidth()));
b.append(String.format("Screen Height: %s\n", wm.getDefaultDisplay().getHeight()));
b.append(String.format("SD Card state: %s\n\n", Environment.getExternalStorageState()));
return b.toString();
}
@NonNull
public File dumpBugReportToFile() throws IOException
{
String date = DateUtils.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;
}
@NonNull
public String getBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
return deviceInfo + "\n" + logcat;
}
public void scheduleReminders()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
ReminderUtils.createReminderAlarms(getContext());
}
}.execute();
}
public void updateWidgets()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
WidgetManager.updateWidgets(getContext());
}
}.execute();
}
}

@ -28,7 +28,9 @@ public class HabitsBackupAgent extends BackupAgentHelper
@Override
public void onCreate()
{
addHelper("preferences", new SharedPreferencesBackupHelper(this, "preferences"));
addHelper("database", new FileBackupHelper(this, "../databases/uhabits.db"));
addHelper("preferences",
new SharedPreferencesBackupHelper(this, "preferences"));
addHelper("database",
new FileBackupHelper(this, "../databases/uhabits.db"));
}
}

@ -19,222 +19,13 @@
package org.isoron.uhabits;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ProgressBar;
import org.isoron.uhabits.ui.AndroidProgressBar;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.about.AboutActivity;
import org.isoron.uhabits.ui.habits.list.ListHabitsFragment;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.ui.intro.IntroActivity;
import org.isoron.uhabits.ui.settings.FilePickerDialog;
import org.isoron.uhabits.ui.settings.SettingsActivity;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.File;
public class MainActivity extends BaseActivity
implements ListHabitsFragment.Listener, MainController.Screen
{
private MainController controller;
private ListHabitsFragment listHabitsFragment;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.list_habits_activity);
setupSupportActionBar(false);
FragmentManager fragmentManager = getSupportFragmentManager();
listHabitsFragment = (ListHabitsFragment) fragmentManager.findFragmentById(R.id.fragment1);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
onPreLollipopCreate();
controller = new MainController();
controller.setScreen(this);
controller.setSystem((HabitsApplication) getApplication());
controller.onStartup();
}
private void onPreLollipopCreate()
{
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if(InterfaceUtils.isNightMode()) return;
int color = getResources().getColor(R.color.grey_900);
actionBar.setBackgroundDrawable(new ColorDrawable(color));
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
menu.clear();
getMenuInflater().inflate(R.menu.main_activity, menu);
MenuItem nightModeItem = menu.findItem(R.id.action_night_mode);
nightModeItem.setChecked(InterfaceUtils.isNightMode());
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_night_mode:
toggleNightMode();
return true;
case R.id.action_settings:
showSettingsScreen();
return true;
case R.id.action_about:
showAboutScreen();
return true;
case R.id.action_faq:
showFAQScreen();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (resultCode)
{
case HabitsApplication.RESULT_IMPORT_DATA:
showImportScreen();
break;
case HabitsApplication.RESULT_EXPORT_CSV:
controller.exportCSV();
break;
case HabitsApplication.RESULT_EXPORT_DB:
controller.exportDB();
break;
case HabitsApplication.RESULT_BUG_REPORT:
controller.sendBugReport();
break;
}
}
private void showFAQScreen()
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.helpURL)));
startActivity(intent);
}
private void showAboutScreen()
{
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
}
private void showSettingsScreen()
public class MainActivity extends ListHabitsActivity
{
Intent intent = new Intent(this, SettingsActivity.class);
startActivityForResult(intent, 0);
}
private void toggleNightMode()
{
if(InterfaceUtils.isNightMode())
InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_LIGHT);
else
InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_DARK);
refreshTheme();
}
private void refreshTheme()
{
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
Intent intent = new Intent(MainActivity.this, MainActivity.class);
MainActivity.this.finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
startActivity(intent);
}
}, 500); // Let the menu disappear first
}
@Override
public void onHabitClick(Habit habit)
{
showHabitScreen(habit);
}
private void showHabitScreen(Habit habit)
{
Intent intent = new Intent(this, ShowHabitActivity.class);
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
startActivity(intent);
}
public void showIntroScreen()
{
Intent intent = new Intent(this, IntroActivity.class);
this.startActivity(intent);
}
public void showImportScreen()
{
File dir = FileUtils.getFilesDir(null);
if(dir == null)
{
showMessage(R.string.could_not_import);
return;
}
FilePickerDialog picker = new FilePickerDialog(this, dir);
picker.setListener(new FilePickerDialog.OnFileSelectedListener()
{
@Override
public void onFileSelected(File file)
{
controller.importData(file);
}
});
picker.show();
}
@Override
public void refresh(Long refreshKey)
{
listHabitsFragment.refresh(refreshKey);
}
@Override
public ProgressBar getProgressBar()
{
return new AndroidProgressBar(listHabitsFragment.getProgressBar());
}
/*
* Since changing the main activity on the manifest file causes things to
* break we always point the launcher icon to this activity instead, and
* redirect to the desired one using inheritance.
*/
}

@ -1,180 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
import org.isoron.uhabits.tasks.ProgressBar;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.Preferences;
import java.io.File;
import java.io.IOException;
public class MainController implements ImportDataTask.Listener, ExportCSVTask.Listener,
ExportDBTask.Listener
{
public interface Screen
{
void showIntroScreen();
void showMessage(Integer stringId);
void refresh(Long refreshKey);
void sendFile(String filename);
void sendEmail(String to, String subject, String content);
ProgressBar getProgressBar();
}
public interface System
{
void scheduleReminders();
void updateWidgets();
File dumpBugReportToFile() throws IOException;
String getBugReport() throws IOException;
}
System sys;
Screen screen;
Preferences prefs;
public MainController()
{
prefs = Preferences.getInstance();
}
public void setScreen(Screen screen)
{
this.screen = screen;
}
public void setSystem(System sys)
{
this.sys = sys;
}
public void onStartup()
{
prefs.initialize();
prefs.incrementLaunchCount();
prefs.updateLastAppVersion();
if(prefs.isFirstRun()) onFirstRun();
sys.updateWidgets();
sys.scheduleReminders();
}
private void onFirstRun()
{
prefs.setFirstRun(false);
prefs.setLastHintTimestamp(DateUtils.getStartOfToday());
screen.showIntroScreen();
}
public void importData(File file)
{
ImportDataTask task = new ImportDataTask(file, screen.getProgressBar());
task.setListener(this);
task.execute();
}
@Override
public void onImportDataFinished(int result)
{
switch (result)
{
case ImportDataTask.SUCCESS:
screen.refresh(null);
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;
}
}
public void exportCSV()
{
ExportCSVTask task = new ExportCSVTask(Habit.getAll(true), screen.getProgressBar());
task.setListener(this);
task.execute();
}
@Override
public void onExportCSVFinished(String filename)
{
if(filename != null) screen.sendFile(filename);
else screen.showMessage(R.string.could_not_export);
}
public void exportDB()
{
ExportDBTask task = new ExportDBTask(screen.getProgressBar());
task.setListener(this);
task.execute();
}
@Override
public void onExportDBFinished(String filename)
{
if(filename != null) screen.sendFile(filename);
else screen.showMessage(R.string.could_not_export);
}
public void sendBugReport()
{
try
{
sys.dumpBugReportToFile();
}
catch (IOException e)
{
// ignored
}
try
{
String log = "---------- BUG REPORT BEGINS ----------\n";
log += sys.getBugReport();
log += "---------- BUG REPORT ENDS ------------\n";
String to = "dev@loophabits.org";
String subject = "Bug Report - Loop Habit Tracker";
screen.sendEmail(log, to, subject);
}
catch (IOException e)
{
e.printStackTrace();
screen.showMessage(R.string.bug_report_failed);
}
}
}

@ -19,29 +19,30 @@
package org.isoron.uhabits.commands;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.tasks.BaseTask;
import java.util.LinkedList;
public class CommandRunner
{
public interface Listener
{
void onCommandExecuted(Command command, Long refreshKey);
}
private static CommandRunner singleton;
private LinkedList<Listener> listeners;
private CommandRunner()
public CommandRunner()
{
listeners = new LinkedList<>();
}
public static CommandRunner getInstance()
private static CommandRunner getInstance()
{
if(singleton == null) singleton = new CommandRunner();
return singleton;
return null;
}
public void addListener(Listener l)
{
listeners.add(l);
}
public void execute(final Command command, final Long refreshKey)
@ -65,13 +66,14 @@ public class CommandRunner
}.execute();
}
public void addListener(Listener l)
public void removeListener(Listener l)
{
listeners.add(l);
listeners.remove(l);
}
public void removeListener(Listener l)
public interface Listener
{
listeners.remove(l);
void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey);
}
}

@ -47,7 +47,7 @@ public class ModelObservable
listeners.remove(l);
}
void notifyListeners()
public void notifyListeners()
{
for(Listener l : listeners) l.onModelChange();
}

@ -19,12 +19,18 @@
package org.isoron.uhabits.tasks;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Habit;
import javax.inject.Inject;
public class ToggleRepetitionTask extends BaseTask
{;
{
@Inject
CommandRunner commandRunner;
public interface Listener {
void onToggleRepetitionFinished();
}
@ -37,13 +43,14 @@ public class ToggleRepetitionTask extends BaseTask
{
this.timestamp = timestamp;
this.habit = habit;
HabitsApplication.getComponent().inject(this);
}
@Override
protected void doInBackground()
{
ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp);
CommandRunner.getInstance().execute(command, habit.getId());
commandRunner.execute(command, habit.getId());
}
@Override

@ -19,188 +19,114 @@
package org.isoron.uhabits.ui;
import android.app.backup.BackupManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Toast;
import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.widgets.WidgetManager;
import java.io.File;
abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler,
CommandRunner.Listener
{
private Toast toast;
import android.view.Menu;
import android.view.MenuItem;
Thread.UncaughtExceptionHandler androidExceptionHandler;
import org.isoron.uhabits.utils.InterfaceUtils;
@Override
protected void onCreate(Bundle savedInstanceState)
/**
* Base class for all activities in the application.
*/
abstract public class BaseActivity extends AppCompatActivity
implements Thread.UncaughtExceptionHandler
{
super.onCreate(savedInstanceState);
@Nullable
private BaseMenu baseMenu;
InterfaceUtils.applyCurrentTheme(this);
@Nullable
private Thread.UncaughtExceptionHandler androidExceptionHandler;
androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Nullable
private BaseScreen screen;
@Override
protected void onResume()
public boolean onCreateOptionsMenu(@Nullable Menu menu)
{
super.onResume();
CommandRunner.getInstance().addListener(this);
if (menu == null) return false;
if (baseMenu == null) return false;
return baseMenu.onCreate(getMenuInflater(), menu);
}
@Override
protected void onPause()
public boolean onOptionsItemSelected(@Nullable MenuItem item)
{
CommandRunner.getInstance().removeListener(this);
super.onPause();
if (item == null) return false;
if (baseMenu == null) return false;
return baseMenu.onItemSelected(item);
}
public void showMessage(Integer stringId)
public void setBaseMenu(@Nullable BaseMenu baseMenu)
{
if (stringId == null) return;
if (toast == null) toast = Toast.makeText(this, stringId, Toast.LENGTH_SHORT);
else toast.setText(stringId);
toast.show();
this.baseMenu = baseMenu;
}
protected void setupSupportActionBar(boolean homeButtonEnabled)
public void setScreen(@Nullable BaseScreen screen)
{
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
if(toolbar == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
toolbar.setElevation(InterfaceUtils.dpToPixels(this, 2));
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if(homeButtonEnabled)
actionBar.setDisplayHomeAsUpEnabled(true);
this.screen = screen;
}
@Override
public void uncaughtException(Thread thread, Throwable ex)
public void uncaughtException(@Nullable Thread thread,
@Nullable Throwable ex)
{
if (ex == null) return;
try
{
ex.printStackTrace();
((HabitsApplication) getApplication()).dumpBugReportToFile();
}
catch(Exception e)
new BaseSystem(this).dumpBugReportToFile();
} catch (Exception e)
{
// ignored
}
if (androidExceptionHandler != null)
androidExceptionHandler.uncaughtException(thread, ex);
else
System.exit(1);
}
protected void setupActionBarColor(int color)
{
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if (!InterfaceUtils.getStyledBoolean(this, R.attr.useHabitColorAsPrimary)) return;
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f);
getWindow().setStatusBarColor(darkerColor);
}
else System.exit(1);
}
@Override
protected void onPostResume()
{
super.onPostResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
hideFakeToolbarShadow();
}
protected void hideFakeToolbarShadow()
{
View view = findViewById(R.id.toolbarShadow);
if(view != null) view.setVisibility(View.GONE);
view = findViewById(R.id.headerShadow);
if(view != null) view.setVisibility(View.GONE);
}
private void dismissNotifications(Context context)
{
for(Habit h : Habit.getHabitsWithReminder())
{
if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED)
HabitBroadcastReceiver.dismissNotification(context, h);
}
}
protected void onActivityResult(int request, int result, Intent data)
{
if (screen == null) return;
screen.onResult(request, result, data);
}
// @Override
// public void onCommandExecuted(Command command, Long refreshKey)
// {
// window.showMessage(command.getExecuteStringId());
// new BaseTask()
// {
// @Override
// protected void doInBackground()
// {
// dismissNotifications(BaseActivity.this);
// BackupManager.dataChanged("org.isoron.uhabits");
// WidgetManager.updateWidgets(BaseActivity.this);
// }
// }.execute();
// }
// private void dismissNotifications(Context context)
// {
// for (Habit h : Habit.getHabitsWithReminder())
// {
// if (h.checkmarks.getTodayValue() != Checkmark.UNCHECKED)
// HabitBroadcastReceiver.dismissNotification(context, h);
// }
// }
@Override
public void onCommandExecuted(Command command, Long refreshKey)
{
showMessage(command.getExecuteStringId());
new BaseTask()
{
@Override
protected void doInBackground()
{
dismissNotifications(BaseActivity.this);
BackupManager.dataChanged("org.isoron.uhabits");
WidgetManager.updateWidgets(BaseActivity.this);
}
}.execute();
}
public void sendFile(@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)));
startActivity(intent);
}
public void sendEmail(String to, String subject, String content)
protected void onCreate(Bundle savedInstanceState)
{
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);
startActivity(intent);
super.onCreate(savedInstanceState);
InterfaceUtils.applyCurrentTheme(this);
androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
}

@ -0,0 +1,60 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.support.annotation.NonNull;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
public abstract class BaseMenu
{
private final BaseActivity activity;
public BaseMenu(BaseActivity activity)
{
this.activity = activity;
}
public final boolean onCreate(@NonNull MenuInflater inflater,
@NonNull Menu menu)
{
menu.clear();
inflater.inflate(getMenuResourceId(), menu);
onCreate(menu);
return true;
}
public void onCreate(@NonNull Menu menu)
{
}
public boolean onItemSelected(@NonNull MenuItem item)
{
return true;
}
protected abstract int getMenuResourceId();
public void invalidate()
{
activity.invalidateOptionsMenu();
}
}

@ -0,0 +1,110 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.InterfaceUtils;
public abstract class BaseRootView extends FrameLayout
{
private final Context context;
public BaseRootView(Context context)
{
super(context);
this.context = context;
}
public BaseRootView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.context = context;
}
public BaseRootView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
this.context = context;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public BaseRootView(Context context,
AttributeSet attrs,
int defStyleAttr,
int defStyleRes)
{
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
}
public boolean getDisplayHomeAsUp()
{
return false;
}
@Nullable
public ProgressBar getProgressBar()
{
return null;
}
@NonNull
public abstract Toolbar getToolbar();
public int getToolbarColor()
{
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
// {
// if (InterfaceUtils.isNightMode()) return;
// int color = activity.getResources().getColor(R.color.grey_900);
// }
// if (!InterfaceUtils.getStyledBoolean(activity, R.attr.useHabitColorAsPrimary)) return;
return Color.BLACK;
}
private void hideFakeToolbarShadow()
{
View view = findViewById(R.id.toolbarShadow);
if (view != null) view.setVisibility(View.GONE);
view = findViewById(R.id.headerShadow);
if (view != null) view.setVisibility(View.GONE);
}
protected void initToolbar()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2));
hideFakeToolbarShadow();
}
}

@ -0,0 +1,217 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDialogFragment;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import org.isoron.uhabits.tasks.ProgressBar;
import org.isoron.uhabits.utils.ColorUtils;
import java.io.File;
public abstract class BaseScreen
{
protected BaseActivity activity;
private Toast toast;
@Nullable
private BaseRootView rootView;
@Nullable
private BaseSelectionMenu selectionMenu;
public BaseScreen(BaseActivity activity)
{
this.activity = activity;
}
public void finishSelection()
{
if (selectionMenu == null) return;
selectionMenu.finish();
}
@Nullable
public ProgressBar getProgressBar()
{
if (rootView == null) return null;
return new ProgressBarWrapper(rootView.getProgressBar());
}
public void invalidate()
{
if (rootView == null) return;
rootView.invalidate();
}
public void onResult(int requestCode, int resultCode, Intent data)
{
}
public void setMenu(@Nullable BaseMenu menu)
{
activity.setBaseMenu(menu);
}
public void setRootView(@Nullable BaseRootView rootView)
{
this.rootView = rootView;
activity.setContentView(rootView);
if (rootView == null) return;
initToolbar();
}
/**
* Set 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;
}
public void showMessage(@Nullable Integer stringId)
{
if (stringId == null) return;
if (toast == null)
toast = Toast.makeText(activity, stringId, Toast.LENGTH_SHORT);
else toast.setText(stringId);
toast.show();
}
public void showSendEmailScreen(String to, String subject, String content)
{
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 initToolbar()
{
if (rootView == null) return;
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);
}
private void setActionBarColor(@NonNull ActionBar actionBar, int color)
{
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
}
private void setStatusBarColor(int baseColor)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
int darkerColor =
ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f);
activity.getWindow().setStatusBarColor(darkerColor);
}
}
protected void showDialog(AppCompatDialogFragment dialog, String tag)
{
dialog.show(activity.getSupportFragmentManager(), tag);
}
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.onDestroy();
}
@Override
public boolean onPrepareActionMode(@Nullable ActionMode mode,
@Nullable Menu menu)
{
if (selectionMenu == null || menu == null) return false;
return selectionMenu.onPrepare(menu);
}
}
}

@ -0,0 +1,87 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
public abstract class BaseSelectionMenu
{
@Nullable
private ActionMode actionMode;
/**
* Finishes the selection operation.
*/
public void finish()
{
if (actionMode != null) actionMode.finish();
}
public void invalidate()
{
if (actionMode != null) actionMode.invalidate();
}
public final void onCreate(@NonNull MenuInflater menuInflater,
@NonNull ActionMode mode,
@NonNull Menu menu)
{
this.actionMode = mode;
menuInflater.inflate(getResourceId(), menu);
onCreate(menu);
}
public void onDestroy()
{
}
public boolean onItemClicked(@NonNull MenuItem item)
{
return false;
}
public boolean onPrepare(@NonNull Menu menu)
{
return false;
}
public void setTitle(String title)
{
if (actionMode != null) actionMode.setTitle(title);
}
protected abstract int getResourceId();
/**
* Called when the menu is first created, right after the menu has been
* inflated.
*
* @param menu the menu containing the buttons
*/
protected void onCreate(@NonNull Menu menu)
{
}
}

@ -0,0 +1,166 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.view.WindowManager;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.widgets.WidgetManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
public class BaseSystem
{
private Context context;
public BaseSystem(Context context)
{
this.context = context;
}
public String getLogcat() throws IOException
{
int maxNLines = 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<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if (log.size() > maxNLines) log.removeFirst();
}
for (String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
public String getDeviceInfo()
{
if (context == null) return "";
StringBuilder b = new StringBuilder();
WindowManager wm =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
b.append(
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME));
b.append(
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE));
b.append(String.format("OS Version: %s (%s)\n",
java.lang.System.getProperty("os.version"),
android.os.Build.VERSION.INCREMENTAL));
b.append(
String.format("OS API Level: %s\n", android.os.Build.VERSION.SDK));
b.append(String.format("Device: %s\n", android.os.Build.DEVICE));
b.append(
String.format("Model (Product): %s (%s)\n", android.os.Build.MODEL,
android.os.Build.PRODUCT));
b.append(
String.format("Manufacturer: %s\n", android.os.Build.MANUFACTURER));
b.append(String.format("Other tags: %s\n", android.os.Build.TAGS));
b.append(String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()));
b.append(String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()));
b.append(String.format("SD Card state: %s\n\n",
Environment.getExternalStorageState()));
return b.toString();
}
@NonNull
public File dumpBugReportToFile() throws IOException
{
String date =
DateUtils.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;
}
@NonNull
public String getBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
return deviceInfo + "\n" + logcat;
}
public void scheduleReminders()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
ReminderUtils.createReminderAlarms(context);
}
}.execute();
}
public void updateWidgets()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
WidgetManager.updateWidgets(context);
}
}.execute();
}
}

@ -1,81 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DateUtils;
public class HintManager
{
private Context context;
private SharedPreferences prefs;
private View hintView;
public HintManager(Context context, View hintView)
{
this.context = context;
this.hintView = hintView;
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public void dismissHint()
{
hintView.animate().alpha(0f).setDuration(500).setListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(Animator animation)
{
hintView.setVisibility(View.GONE);
}
});
}
public void showHintIfAppropriate()
{
Integer lastHintNumber = prefs.getInt("last_hint_number", -1);
Long lastHintTimestamp = prefs.getLong("last_hint_timestamp", -1);
if (DateUtils.getStartOfToday() > lastHintTimestamp) showHint(lastHintNumber + 1);
}
private void showHint(int hintNumber)
{
String[] hints = context.getResources().getStringArray(R.array.hints);
if (hintNumber >= hints.length) return;
prefs.edit().putInt("last_hint_number", hintNumber).apply();
prefs.edit().putLong("last_hint_timestamp", DateUtils.getStartOfToday()).apply();
TextView tvContent = (TextView) hintView.findViewById(R.id.hintContent);
tvContent.setText(hints[hintNumber]);
hintView.setAlpha(0.0f);
hintView.setVisibility(View.VISIBLE);
hintView.animate().alpha(1f).setDuration(500);
}
}

@ -23,11 +23,11 @@ import android.view.View;
import org.isoron.uhabits.tasks.ProgressBar;
public class AndroidProgressBar implements ProgressBar
public class ProgressBarWrapper implements ProgressBar
{
private final android.widget.ProgressBar progressBar;
public AndroidProgressBar(android.widget.ProgressBar progressBar)
public ProgressBarWrapper(android.widget.ProgressBar progressBar)
{
this.progressBar = progressBar;
}

@ -30,32 +30,12 @@ import org.isoron.uhabits.R;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.utils.InterfaceUtils;
/**
* 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 implements View.OnClickListener
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
setupSupportActionBar(true);
int color = InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor);
setupActionBarColor(color);
TextView tvVersion = (TextView) findViewById(R.id.tvVersion);
TextView tvRate = (TextView) findViewById(R.id.tvRate);
TextView tvFeedback = (TextView) findViewById(R.id.tvFeedback);
TextView tvSource = (TextView) findViewById(R.id.tvSource);
tvVersion.setText(String.format(getResources().getString(R.string.version_n),
BuildConfig.VERSION_NAME));
tvRate.setOnClickListener(this);
tvFeedback.setOnClickListener(this);
tvSource.setOnClickListener(this);
}
@Override
public void onClick(View v)
{
@ -89,4 +69,29 @@ public class AboutActivity extends BaseActivity implements View.OnClickListener
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
// setupSupportActionBar(true);
int color =
InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor);
// setupActionBarColor(color);
TextView tvVersion = (TextView) findViewById(R.id.tvVersion);
TextView tvRate = (TextView) findViewById(R.id.tvRate);
TextView tvFeedback = (TextView) findViewById(R.id.tvFeedback);
TextView tvSource = (TextView) findViewById(R.id.tvSource);
tvVersion.setText(
String.format(getResources().getString(R.string.version_n),
BuildConfig.VERSION_NAME));
tvRate.setOnClickListener(this);
tvFeedback.setOnClickListener(this);
tvSource.setOnClickListener(this);
}
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains classes for AboutActivity
*/
package org.isoron.uhabits.ui.about;

@ -32,7 +32,9 @@ import com.android.colorpicker.ColorPickerSwatch;
import com.android.datetimepicker.time.RadialPickerLayout;
import com.android.datetimepicker.time.TimePickerDialog;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils;
@ -40,6 +42,8 @@ import org.isoron.uhabits.utils.Preferences;
import java.util.Arrays;
import javax.inject.Inject;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnItemSelected;
@ -47,15 +51,24 @@ import butterknife.OnItemSelected;
public abstract class BaseDialogFragment extends AppCompatDialogFragment
{
protected Habit originalHabit;
protected Habit modifiedHabit;
protected Preferences prefs = Preferences.getInstance();
protected BaseDialogHelper helper;
@Inject
Preferences prefs;
@Inject
CommandRunner commandRunner;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.edit_habit, container, false);
HabitsApplication.getComponent().inject(this);
ButterKnife.bind(this, view);
helper = new BaseDialogHelper(this, view);
@ -66,11 +79,16 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
return view;
}
protected abstract void initializeHabits();
protected abstract void saveHabit();
protected abstract int getTitle();
@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.freqNum = freqNums[position];
modifiedHabit.freqDen = freqDens[position];
helper.populateFrequencyFields(modifiedHabit);
}
@Override
@SuppressWarnings("ConstantConditions")
@ -86,15 +104,9 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
}
}
protected void restoreSavedInstance(@Nullable Bundle bundle)
{
if(bundle == null) return;
modifiedHabit.color = bundle.getInt("color", modifiedHabit.color);
modifiedHabit.reminderMin = bundle.getInt("reminderMin", -1);
modifiedHabit.reminderHour = bundle.getInt("reminderHour", -1);
modifiedHabit.reminderDays = bundle.getInt("reminderDays", -1);
if(modifiedHabit.reminderMin < 0) modifiedHabit.clearReminder();
}
protected abstract int getTitle();
protected abstract void initializeHabits();
@OnClick(R.id.buttonDiscard)
void onButtonDiscardClick()
@ -102,15 +114,6 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
dismiss();
}
@OnClick(R.id.buttonSave)
void onSaveButtonClick()
{
helper.parseFormIntoHabit(modifiedHabit);
if (!helper.validate(modifiedHabit)) return;
saveHabit();
dismiss();
}
@OnClick(R.id.tvReminderTime)
@SuppressWarnings("ConstantConditions")
void onDateSpinnerClick()
@ -127,6 +130,15 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
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()
@ -134,29 +146,33 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
if (!modifiedHabit.hasReminder()) return;
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
dialog.setListener(new OnWeekdaysPickedListener());
dialog.setSelectedDays(DateUtils.unpackWeekdayList(modifiedHabit.reminderDays));
dialog.setSelectedDays(
DateUtils.unpackWeekdayList(modifiedHabit.reminderDays));
dialog.show(getFragmentManager(), "weekdayPicker");
}
@OnItemSelected(R.id.sFrequency)
public void onFrequencySelected(int position)
protected void restoreSavedInstance(@Nullable Bundle bundle)
{
if(position < 0 || position > 4) throw new IllegalArgumentException();
int freqNums[] = { 1, 1, 2, 5, 3 };
int freqDens[] = { 1, 7, 7, 7, 7 };
modifiedHabit.freqNum = freqNums[position];
modifiedHabit.freqDen = freqDens[position];
helper.populateFrequencyFields(modifiedHabit);
if (bundle == null) return;
modifiedHabit.color = bundle.getInt("color", modifiedHabit.color);
modifiedHabit.reminderMin = bundle.getInt("reminderMin", -1);
modifiedHabit.reminderHour = bundle.getInt("reminderHour", -1);
modifiedHabit.reminderDays = bundle.getInt("reminderDays", -1);
if (modifiedHabit.reminderMin < 0) modifiedHabit.clearReminder();
}
protected abstract void saveHabit();
@OnClick(R.id.buttonPickColor)
void showColorPicker()
{
int androidColor = ColorUtils.getColor(getContext(), modifiedHabit.color);
int androidColor =
ColorUtils.getColor(getContext(), modifiedHabit.color);
ColorPickerDialog picker = ColorPickerDialog.newInstance(
R.string.color_picker_default_title, ColorUtils.getPalette(getContext()),
androidColor, 4, ColorPickerDialog.SIZE_SMALL);
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,
ColorUtils.getPalette(getContext()), androidColor, 4,
ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(new OnColorSelectedListener());
picker.show(getFragmentManager(), "picker");
@ -165,43 +181,36 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
private void showTimePicker(int defaultHour, int defaultMin)
{
boolean is24HourMode = DateFormat.is24HourFormat(getContext());
TimePickerDialog timePicker = TimePickerDialog.newInstance(new OnTimeSetListener(),
defaultHour, defaultMin, is24HourMode);
TimePickerDialog timePicker =
TimePickerDialog.newInstance(new OnTimeSetListener(), defaultHour,
defaultMin, is24HourMode);
timePicker.show(getFragmentManager(), "timePicker");
}
private class OnColorSelectedListener implements ColorPickerSwatch.OnColorSelectedListener
private class OnColorSelectedListener
implements ColorPickerSwatch.OnColorSelectedListener
{
@Override
public void onColorSelected(int androidColor)
{
int paletteColor = ColorUtils.colorToPaletteIndex(getActivity(), androidColor);
int paletteColor =
ColorUtils.colorToPaletteIndex(getActivity(), androidColor);
prefs.setDefaultHabitColor(paletteColor);
modifiedHabit.color = paletteColor;
helper.populateColor(paletteColor);
}
}
private class OnWeekdaysPickedListener implements WeekdayPickerDialog.OnWeekdaysPickedListener
private class OnTimeSetListener
implements TimePickerDialog.OnTimeSetListener
{
@Override
public void onWeekdaysPicked(boolean[] selectedDays)
public void onTimeCleared(RadialPickerLayout view)
{
if(isSelectionEmpty(selectedDays))
Arrays.fill(selectedDays, true);
modifiedHabit.reminderDays = DateUtils.packWeekdayList(selectedDays);
modifiedHabit.clearReminder();
helper.populateReminderFields(modifiedHabit);
}
private boolean isSelectionEmpty(boolean[] selectedDays)
{
for (boolean d : selectedDays) if (d) return false;
return true;
}
}
private class OnTimeSetListener implements TimePickerDialog.OnTimeSetListener
{
@Override
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
{
@ -210,12 +219,25 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
modifiedHabit.reminderDays = DateUtils.ALL_WEEK_DAYS;
helper.populateReminderFields(modifiedHabit);
}
}
private class OnWeekdaysPickedListener
implements WeekdayPickerDialog.OnWeekdaysPickedListener
{
@Override
public void onTimeCleared(RadialPickerLayout view)
public void onWeekdaysPicked(boolean[] selectedDays)
{
modifiedHabit.clearReminder();
if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true);
modifiedHabit.reminderDays =
DateUtils.packWeekdayList(selectedDays);
helper.populateReminderFields(modifiedHabit);
}
private boolean isSelectionEmpty(boolean[] selectedDays)
{
for (boolean d : selectedDays) if (d) return false;
return true;
}
}
}

@ -37,15 +37,33 @@ import butterknife.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;
@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)
{
@ -53,37 +71,30 @@ public class BaseDialogHelper
ButterKnife.bind(this, view);
}
protected void populateForm(final Habit habit)
void parseFormIntoHabit(Habit habit)
{
if(habit.name != null) tvName.setText(habit.name);
if(habit.description != null) tvDescription.setText(habit.description);
populateColor(habit.color);
populateFrequencyFields(habit);
populateReminderFields(habit);
habit.name = tvName.getText().toString().trim();
habit.description = tvDescription.getText().toString().trim();
String freqNum = tvFreqNum.getText().toString();
String freqDen = tvFreqDen.getText().toString();
if (!freqNum.isEmpty()) habit.freqNum = Integer.parseInt(freqNum);
if (!freqDen.isEmpty()) habit.freqDen = Integer.parseInt(freqDen);
}
void populateColor(int paletteColor)
{
tvName.setTextColor(ColorUtils.getColor(frag.getContext(), paletteColor));
tvName.setTextColor(
ColorUtils.getColor(frag.getContext(), paletteColor));
}
@SuppressWarnings("ConstantConditions")
void populateReminderFields(Habit habit)
{
if (!habit.hasReminder())
protected void populateForm(final Habit habit)
{
tvReminderTime.setText(R.string.reminder_off);
llReminderDays.setVisibility(View.GONE);
return;
}
String time = DateUtils.formatTime(frag.getContext(), habit.reminderHour, habit.reminderMin);
tvReminderTime.setText(time);
llReminderDays.setVisibility(View.VISIBLE);
if (habit.name != null) tvName.setText(habit.name);
if (habit.description != null) tvDescription.setText(habit.description);
boolean weekdays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
tvReminderDays.setText(DateUtils.formatWeekdayList(frag.getContext(), weekdays));
populateColor(habit.color);
populateFrequencyFields(habit);
populateReminderFields(habit);
}
@SuppressLint("SetTextI18n")
@ -91,8 +102,7 @@ public class BaseDialogHelper
{
int quickSelectPosition = -1;
if(habit.freqNum.equals(habit.freqDen))
quickSelectPosition = 0;
if (habit.freqNum.equals(habit.freqDen)) quickSelectPosition = 0;
else if (habit.freqNum == 1 && habit.freqDen == 7)
quickSelectPosition = 1;
@ -103,13 +113,36 @@ public class BaseDialogHelper
else if (habit.freqNum == 5 && habit.freqDen == 7)
quickSelectPosition = 3;
if(quickSelectPosition >= 0) showSimplifiedFrequency(quickSelectPosition);
if (quickSelectPosition >= 0)
showSimplifiedFrequency(quickSelectPosition);
else showCustomFrequency();
tvFreqNum.setText(habit.freqNum.toString());
tvFreqDen.setText(habit.freqDen.toString());
}
@SuppressWarnings("ConstantConditions")
void populateReminderFields(Habit habit)
{
if (!habit.hasReminder())
{
tvReminderTime.setText(R.string.reminder_off);
llReminderDays.setVisibility(View.GONE);
return;
}
String time =
DateUtils.formatTime(frag.getContext(), habit.reminderHour,
habit.reminderMin);
tvReminderTime.setText(time);
llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
tvReminderDays.setText(
DateUtils.formatWeekdayList(frag.getContext(), weekdays));
}
private void showCustomFrequency()
{
sFrequency.setVisibility(View.GONE);
@ -130,32 +163,25 @@ public class BaseDialogHelper
if (habit.name.length() == 0)
{
tvName.setError(frag.getString(R.string.validation_name_should_not_be_blank));
tvName.setError(
frag.getString(R.string.validation_name_should_not_be_blank));
valid = false;
}
if (habit.freqNum <= 0)
{
tvFreqNum.setError(frag.getString(R.string.validation_number_should_be_positive));
tvFreqNum.setError(
frag.getString(R.string.validation_number_should_be_positive));
valid = false;
}
if (habit.freqNum > habit.freqDen)
{
tvFreqNum.setError(frag.getString(R.string.validation_at_most_one_rep_per_day));
tvFreqNum.setError(
frag.getString(R.string.validation_at_most_one_rep_per_day));
valid = false;
}
return valid;
}
void parseFormIntoHabit(Habit habit)
{
habit.name = tvName.getText().toString().trim();
habit.description = tvDescription.getText().toString().trim();
String freqNum = tvFreqNum.getText().toString();
String freqDen = tvFreqDen.getText().toString();
if(!freqNum.isEmpty()) habit.freqNum = Integer.parseInt(freqNum);
if(!freqDen.isEmpty()) habit.freqDen = Integer.parseInt(freqDen);
}
}

@ -21,7 +21,6 @@ package org.isoron.uhabits.ui.habits.edit;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.models.Habit;
@ -45,6 +44,6 @@ public class CreateHabitDialogFragment extends BaseDialogFragment
protected void saveHabit()
{
Command command = new CreateHabitCommand(modifiedHabit);
CommandRunner.getInstance().execute(command, null);
commandRunner.execute(command, null);
}
}

@ -23,7 +23,6 @@ import android.os.Bundle;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit;
@ -48,7 +47,8 @@ public class EditHabitDialogFragment extends BaseDialogFragment
protected void initializeHabits()
{
Long habitId = (Long) getArguments().get("habitId");
if(habitId == null) throw new IllegalArgumentException("habitId must be specified");
if (habitId == null)
throw new IllegalArgumentException("habitId must be specified");
originalHabit = Habit.get(habitId);
modifiedHabit = new Habit(originalHabit);
@ -57,6 +57,6 @@ public class EditHabitDialogFragment extends BaseDialogFragment
protected void saveHabit()
{
Command command = new EditHabitCommand(originalHabit, modifiedHabit);
CommandRunner.getInstance().execute(command, originalHabit.getId());
commandRunner.execute(command, originalHabit.getId());
}
}

@ -36,7 +36,9 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener
{
private Habit habit;
private Listener listener;
HabitHistoryView historyView;
@Override
@ -51,13 +53,15 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
if (id > 0) this.habit = Habit.get(id);
}
int padding = (int) getResources().getDimension(R.dimen.history_editor_padding);
int padding =
(int) getResources().getDimension(R.dimen.history_editor_padding);
historyView.setPadding(padding, 0, padding, 0);
historyView.setHabit(habit);
historyView.setIsEditable(true);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.history)
builder
.setTitle(R.string.history)
.setView(historyView)
.setPositiveButton(android.R.string.ok, this);
@ -84,7 +88,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
super.onResume();
DisplayMetrics metrics = getResources().getDisplayMetrics();
int maxHeight = getResources().getDimensionPixelSize(R.dimen.history_editor_max_height);
int maxHeight = getResources().getDimensionPixelSize(
R.dimen.history_editor_max_height);
int width = metrics.widthPixels;
int height = Math.min(metrics.heightPixels, maxHeight);
@ -121,7 +126,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
this.listener = listener;
}
public interface Listener {
public interface Listener
{
void onHistoryEditorClosed();
}
}

@ -28,8 +28,9 @@ import android.support.v7.app.AppCompatDialogFragment;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DateUtils;
public class WeekdayPickerDialog extends AppCompatDialogFragment
implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener
public class WeekdayPickerDialog extends AppCompatDialogFragment implements
DialogInterface.OnMultiChoiceClickListener,
DialogInterface.OnClickListener
{
public interface OnWeekdaysPickedListener
@ -38,6 +39,7 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment
}
private boolean[] selectedDays;
private OnWeekdaysPickedListener listener;
public void setListener(OnWeekdaysPickedListener listener)
@ -54,10 +56,13 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment
public Dialog onCreateDialog(Bundle savedInstanceState)
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.select_weekdays)
.setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays, this)
builder
.setTitle(R.string.select_weekdays)
.setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays,
this)
.setPositiveButton(android.R.string.yes, this)
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)

@ -1,99 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import java.util.List;
class HabitListAdapter extends BaseAdapter
{
private LayoutInflater inflater;
private HabitListLoader loader;
private ListHabitsHelper helper;
private List selectedPositions;
private View.OnLongClickListener onCheckmarkLongClickListener;
private View.OnClickListener onCheckmarkClickListener;
public HabitListAdapter(Context context, HabitListLoader loader)
{
this.loader = loader;
inflater = LayoutInflater.from(context);
helper = new ListHabitsHelper(context, loader);
}
@Override
public int getCount()
{
return loader.habits.size();
}
@Override
public Habit getItem(int position)
{
return loader.habitsList.get(position);
}
@Override
public long getItemId(int position)
{
return (getItem(position)).getId();
}
@Override
public View getView(int position, View view, ViewGroup parent)
{
final Habit habit = loader.habitsList.get(position);
boolean selected = selectedPositions.contains(position);
if (view == null || (Long) view.getTag(R.id.timestamp_key) != DateUtils.getStartOfToday())
{
view = helper.inflateHabitCard(inflater, onCheckmarkLongClickListener,
onCheckmarkClickListener);
}
helper.updateHabitCard(view, habit, selected);
return view;
}
public void setSelectedPositions(List selectedPositions)
{
this.selectedPositions = selectedPositions;
}
public void setOnCheckmarkLongClickListener(View.OnLongClickListener listener)
{
this.onCheckmarkLongClickListener = listener;
}
public void setOnCheckmarkClickListener(View.OnClickListener listener)
{
this.onCheckmarkClickListener = listener;
}
}

@ -1,223 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.support.annotation.Nullable;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import java.util.HashMap;
import java.util.List;
public class HabitListLoader implements CommandRunner.Listener
{
public interface Listener
{
void onLoadFinished();
}
private BaseTask currentFetchTask;
private int checkmarkCount;
@Nullable
private Listener listener;
private Long lastLoadTimestamp;
public HashMap<Long, Habit> habits;
public List<Habit> habitsList;
public HashMap<Long, int[]> checkmarks;
public HashMap<Long, Integer> scores;
boolean includeArchived;
public void setIncludeArchived(boolean includeArchived)
{
this.includeArchived = includeArchived;
}
public void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
public Long getLastLoadTimestamp()
{
return lastLoadTimestamp;
}
public HabitListLoader()
{
habits = new HashMap<>();
checkmarks = new HashMap<>();
scores = new HashMap<>();
}
public void reorder(int from, int to)
{
Habit fromHabit = habitsList.get(from);
Habit toHabit = habitsList.get(to);
habitsList.remove(from);
habitsList.add(to, fromHabit);
Habit.reorder(fromHabit, toHabit);
}
public void updateAllHabits(final boolean updateScoresAndCheckmarks)
{
if (currentFetchTask != null) currentFetchTask.cancel(true);
currentFetchTask = new BaseTask()
{
public HashMap<Long, Habit> newHabits;
public HashMap<Long, int[]> newCheckmarks;
public HashMap<Long, Integer> newScores;
public List<Habit> newHabitList;
@Override
protected void doInBackground()
{
newHabits = new HashMap<>();
newCheckmarks = new HashMap<>();
newScores = new HashMap<>();
newHabitList = Habit.getAll(includeArchived);
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay;
int[] empty = new int[checkmarkCount];
for(Habit h : newHabitList)
{
Long id = h.getId();
newHabits.put(id, h);
if(checkmarks.containsKey(id))
newCheckmarks.put(id, checkmarks.get(id));
else
newCheckmarks.put(id, empty);
if(scores.containsKey(id))
newScores.put(id, scores.get(id));
else
newScores.put(id, 0);
}
commit();
if(!updateScoresAndCheckmarks) return;
int current = 0;
for (Habit h : newHabitList)
{
if (isCancelled()) return;
Long id = h.getId();
newScores.put(id, h.scores.getTodayValue());
newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
publishProgress(current++, newHabits.size());
}
}
private void commit()
{
habits = newHabits;
scores = newScores;
checkmarks = newCheckmarks;
habitsList = newHabitList;
}
@Override
protected void onProgressUpdate(Integer... values)
{
if(listener != null) listener.onLoadFinished();
}
@Override
protected void onPostExecute(Void aVoid)
{
if (isCancelled()) return;
lastLoadTimestamp = DateUtils.getStartOfToday();
currentFetchTask = null;
if(listener != null) listener.onLoadFinished();
super.onPostExecute(null);
}
};
currentFetchTask.execute();
}
public void updateHabit(final Long id)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay;
Habit h = Habit.get(id);
if(h == null) return;
habits.put(id, h);
scores.put(id, h.scores.getTodayValue());
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
}
@Override
protected void onPostExecute(Void aVoid)
{
if(listener != null) listener.onLoadFinished();
super.onPostExecute(null);
}
}.execute();
}
public void onResume()
{
CommandRunner.getInstance().addListener(this);
}
public void onPause()
{
CommandRunner.getInstance().removeListener(this);
}
@Override
public void onCommandExecuted(Command command, Long refreshKey)
{
if(refreshKey == null) updateAllHabits(true);
else updateHabit(refreshKey);
}
}

@ -1,229 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.android.colorpicker.ColorPickerDialog;
import com.android.colorpicker.ColorPickerSwatch;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.edit.EditHabitDialogFragment;
import org.isoron.uhabits.utils.ColorUtils;
import java.util.LinkedList;
import java.util.List;
public class HabitListSelectionCallback implements ActionMode.Callback
{
private HabitListLoader loader;
private List<Integer> selectedPositions;
private BaseActivity activity;
private Listener listener;
public interface Listener
{
void onActionModeDestroyed(ActionMode mode);
}
public HabitListSelectionCallback(BaseActivity activity, HabitListLoader loader)
{
this.activity = activity;
this.loader = loader;
selectedPositions = new LinkedList<>();
}
public void setListener(Listener listener)
{
this.listener = listener;
}
public void setSelectedPositions(List<Integer> selectedPositions)
{
this.selectedPositions = selectedPositions;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
activity.getMenuInflater().inflate(R.menu.list_habits_selection, menu);
updateTitle(mode);
updateActions(menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu)
{
updateTitle(mode);
updateActions(menu);
return true;
}
private void updateActions(Menu menu)
{
boolean showEdit = (selectedPositions.size() == 1);
boolean showArchive = true;
boolean showUnarchive = true;
for (int i : selectedPositions)
{
Habit h = loader.habitsList.get(i);
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);
}
private void updateTitle(ActionMode mode)
{
mode.setTitle("" + selectedPositions.size());
}
@Override
public boolean onActionItemClicked(final ActionMode mode, MenuItem item)
{
final LinkedList<Habit> selectedHabits = new LinkedList<>();
for (int i : selectedPositions)
selectedHabits.add(loader.habitsList.get(i));
Habit firstHabit = selectedHabits.getFirst();
switch (item.getItemId())
{
case R.id.action_archive_habit:
archiveHabits(selectedHabits);
mode.finish();
return true;
case R.id.action_unarchive_habit:
unarchiveHabits(selectedHabits);
mode.finish();
return true;
case R.id.action_edit_habit:
{
editHabit(firstHabit);
mode.finish();
return true;
}
case R.id.action_color:
{
showColorPicker(mode, selectedHabits, firstHabit);
mode.finish();
return true;
}
case R.id.action_delete:
{
deleteHabits(mode, selectedHabits);
mode.finish();
return true;
}
}
return false;
}
private void deleteHabits(final ActionMode mode, final LinkedList<Habit> selectedHabits)
{
new AlertDialog.Builder(activity).setTitle(R.string.delete_habits)
.setMessage(R.string.delete_habits_message)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
CommandRunner.getInstance()
.execute(new DeleteHabitsCommand(selectedHabits), null);
mode.finish();
}
})
.setNegativeButton(android.R.string.no, null)
.show();
}
private void showColorPicker(final ActionMode mode, final LinkedList<Habit> selectedHabits,
Habit firstHabit)
{
int originalAndroidColor = ColorUtils.getColor(activity, firstHabit.color);
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,
ColorUtils.getPalette(activity), originalAndroidColor, 4,
ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
{
public void onColorSelected(int androidColor)
{
int paletteColor = ColorUtils.colorToPaletteIndex(activity, androidColor);
CommandRunner.getInstance()
.execute(new ChangeHabitColorCommand(selectedHabits, paletteColor), null);
mode.finish();
}
});
picker.show(activity.getSupportFragmentManager(), "picker");
}
private void editHabit(Habit habit)
{
BaseDialogFragment
frag = EditHabitDialogFragment.newInstance(habit.getId());
frag.show(activity.getSupportFragmentManager(), "editHabit");
}
private void unarchiveHabits(LinkedList<Habit> selectedHabits)
{
CommandRunner.getInstance().execute(new UnarchiveHabitsCommand(selectedHabits), null);
}
private void archiveHabits(LinkedList<Habit> selectedHabits)
{
CommandRunner.getInstance().execute(new ArchiveHabitsCommand(selectedHabits), null);
}
@Override
public void onDestroyActionMode(ActionMode mode)
{
if(listener != null) listener.onActionModeDestroyed(mode);
}
}

@ -1,273 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.widget.AdapterView;
import com.mobeta.android.dslv.DragSortController;
import com.mobeta.android.dslv.DragSortListView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.Preferences;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
public class HabitListView extends DragSortListView implements View.OnClickListener,
View.OnLongClickListener, DragSortListView.DropListener, AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener, DragSortListView.DragListener, HabitListLoader.Listener
{
private final HabitListLoader loader;
private final HabitListAdapter adapter;
private final ListHabitsHelper helper;
private final Preferences prefs;
private final List<Integer> selectedPositions;
@Nullable
private Listener listener;
private long lastLongClick;
private boolean showArchived;
public HabitListView(Context context, AttributeSet attrs)
{
super(context, attrs);
loader = new HabitListLoader();
adapter = new HabitListAdapter(context, loader);
selectedPositions = new LinkedList<>();
prefs = Preferences.getInstance();
helper = new ListHabitsHelper(getContext(), loader);
adapter.setSelectedPositions(selectedPositions);
adapter.setOnCheckmarkClickListener(this);
adapter.setOnCheckmarkLongClickListener(this);
loader.setListener(this);
loader.setCheckmarkCount(helper.getButtonCount());
setAdapter(adapter);
setOnItemClickListener(this);
setOnItemLongClickListener(this);
setDropListener(this);
setDragListener(this);
setFloatViewManager(new HabitsDragSortController());
setDragEnabled(false);
setLongClickable(true);
}
public HabitListLoader getLoader()
{
return loader;
}
public List<Integer> getSelectedPositions()
{
return selectedPositions;
}
public void setListener(@Nullable Listener l)
{
this.listener = l;
}
@Override
public void drop(int from, int to)
{
if(from == to) return;
cancelSelection();
loader.reorder(from, to);
adapter.notifyDataSetChanged();
loader.updateAllHabits(false);
}
@Override
public void onClick(View v)
{
if (v.getId() != R.id.tvCheck) return;
if (prefs.isShortToggleEnabled()) toggleCheckmark(v);
else if(listener != null) listener.onInvalidToggle();
}
@Override
public boolean onLongClick(View v)
{
lastLongClick = new Date().getTime();
if (v.getId() != R.id.tvCheck) return true;
if (prefs.isShortToggleEnabled()) return true;
toggleCheckmark(v);
return true;
}
public void toggleShowArchived()
{
showArchived = !showArchived;
loader.setIncludeArchived(showArchived);
loader.updateAllHabits(true);
}
private void toggleCheckmark(View v)
{
Long id = helper.getHabitIdFromCheckmarkView(v);
Habit habit = loader.habits.get(id);
if(habit == null) return;
float x = v.getX() + v.getWidth() / 2.0f + ((View) v.getParent()).getX();
float y = v.getY() + v.getHeight() / 2.0f + ((View) v.getParent()).getY();
helper.triggerRipple((View) v.getParent().getParent(), x, y);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
helper.toggleCheckmarkView(v, habit);
long timestamp = helper.getTimestampFromCheckmarkView(v);
if(listener != null) listener.onToggleCheckmark(habit, timestamp);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (new Date().getTime() - lastLongClick < 1000) return;
if(selectedPositions.isEmpty())
{
Habit habit = loader.habitsList.get(position);
if(listener != null) listener.onHabitClick(habit);
}
else
{
toggleItemSelected(position);
adapter.notifyDataSetChanged();
}
}
private void toggleItemSelected(int position)
{
int k = selectedPositions.indexOf(position);
if(k < 0) selectedPositions.add(position);
else selectedPositions.remove(k);
if(listener != null)
{
if (selectedPositions.isEmpty()) listener.onHabitSelectionFinish();
else listener.onHabitSelectionChange();
}
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
{
selectHabit(position);
return true;
}
private void selectHabit(int position)
{
if(!selectedPositions.contains(position)) selectedPositions.add(position);
adapter.notifyDataSetChanged();
if(listener != null)
{
if (selectedPositions.size() == 1) listener.onHabitSelectionStart();
else listener.onHabitSelectionChange();
}
}
@Override
public void drag(int from, int to)
{
}
@Override
public void startDrag(int position)
{
selectHabit(position);
}
public boolean getShowArchived()
{
return showArchived;
}
public void cancelSelection()
{
selectedPositions.clear();
adapter.notifyDataSetChanged();
setDragEnabled(true);
if(listener != null) listener.onHabitSelectionFinish();
}
public void refreshData(Long refreshKey)
{
if (refreshKey == null) loader.updateAllHabits(true);
else loader.updateHabit(refreshKey);
}
@Override
public void onLoadFinished()
{
adapter.notifyDataSetChanged();
if(listener != null) listener.onDatasetChanged();
}
private class HabitsDragSortController extends DragSortController
{
public HabitsDragSortController()
{
super(HabitListView.this);
setRemoveEnabled(false);
}
@Override
public View onCreateFloatView(int position)
{
return adapter.getView(position, null, null);
}
@Override
public void onDestroyFloatView(View floatView)
{
}
}
public interface Listener
{
void onToggleCheckmark(Habit habit, long timestamp);
void onHabitClick(Habit habit);
void onHabitSelectionStart();
void onHabitSelectionFinish();
void onHabitSelectionChange();
void onInvalidToggle();
void onDatasetChanged();
}
}

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.os.Bundle;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseSystem;
/**
* Activity that allows the user to see and modify the list of habits.
*/
public class ListHabitsActivity extends BaseActivity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
BaseSystem system = new BaseSystem(this);
ListHabitsScreen screen = new ListHabitsScreen(this);
ListHabitsController controller =
new ListHabitsController(screen, system);
screen.setController(controller);
setScreen(screen);
controller.onStartup();
}
}

@ -19,18 +19,162 @@
package org.isoron.uhabits.ui.habits.list;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
import org.isoron.uhabits.ui.BaseSystem;
import org.isoron.uhabits.ui.habits.list.controllers.HabitCardListController;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.Preferences;
import java.io.File;
import java.io.IOException;
import javax.inject.Inject;
public class ListHabitsController
implements ImportDataTask.Listener, HabitCardListController.HabitListener
{
public interface Screen
@NonNull
private final ListHabitsScreen screen;
@NonNull
private final BaseSystem system;
@Inject
Preferences prefs;
@Inject
CommandRunner commandRunner;
public ListHabitsController(@NonNull ListHabitsScreen screen,
@NonNull BaseSystem system)
{
this.screen = screen;
this.system = system;
HabitsApplication.getComponent().inject(this);
}
public void onExportCSV()
{
ExportCSVTask task =
new ExportCSVTask(Habit.getAll(true), screen.getProgressBar());
task.setListener(filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
});
task.execute();
}
private Screen screen;
public void onExportDB()
{
ExportDBTask task = new ExportDBTask(screen.getProgressBar());
task.setListener(filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
});
task.execute();
}
public void setScreen(Screen screen)
@Override
public void onHabitClick(@NonNull Habit h)
{
this.screen = screen;
screen.showHabitScreen(h);
}
@Override
public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
{
Habit.reorder(from, to);
}
public void onImportData(File file)
{
ImportDataTask task = new ImportDataTask(file, screen.getProgressBar());
task.setListener(this);
task.execute();
}
@Override
public void onImportDataFinished(int result)
{
switch (result)
{
case ImportDataTask.SUCCESS:
screen.invalidate();
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 onSendBugReport()
{
try
{
system.dumpBugReportToFile();
} catch (IOException e)
{
// ignored
}
try
{
String log = "---------- BUG REPORT BEGINS ----------\n";
log += system.getBugReport();
log += "---------- BUG REPORT ENDS ------------\n";
String to = "dev@loophabits.org";
String subject = "Bug Report - Loop Habit Tracker";
screen.showSendEmailScreen(log, to, subject);
} catch (IOException e)
{
e.printStackTrace();
screen.showMessage(R.string.bug_report_failed);
}
}
public void onStartup()
{
prefs.initialize();
prefs.incrementLaunchCount();
prefs.updateLastAppVersion();
if (prefs.isFirstRun()) onFirstRun();
system.updateWidgets();
system.scheduleReminders();
}
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp),
null);
}
private void onFirstRun()
{
prefs.setFirstRun(false);
prefs.updateLastHint(-1, DateUtils.getStartOfToday());
screen.showIntroScreen();
}
}

@ -1,236 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.HintManager;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.edit.CreateHabitDialogFragment;
import org.isoron.uhabits.utils.InterfaceUtils;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class ListHabitsFragment extends Fragment
implements HabitListSelectionCallback.Listener, ListHabitsController.Screen
{
private ActionMode actionMode;
private HintManager hintManager;
private ListHabitsHelper helper;
private Listener habitClickListener;
private BaseActivity activity;
@BindView(R.id.listView) HabitListView listView;
@BindView(R.id.llButtonsHeader) LinearLayout llButtonsHeader;
@BindView(R.id.progressBar) ProgressBar progressBar;
@BindView(R.id.llEmpty) View llEmpty;
@BindView(R.id.llHint) View llHint;
@BindView(R.id.tvStarEmpty) TextView tvStarEmpty;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
ButterKnife.bind(this, view);
helper = new ListHabitsHelper(activity, listView.getLoader());
hintManager = new HintManager(activity, llHint);
tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(activity));
listView.setListener(new HabitListViewListener());
setHasOptionsMenu(true);
return view;
}
@Override
@SuppressWarnings("deprecation")
public void onAttach(Activity activity)
{
super.onAttach(activity);
this.activity = (BaseActivity) activity;
habitClickListener = (Listener) activity;
}
@Override
public void onResume()
{
super.onResume();
listView.getLoader().onResume();
listView.refreshData(null);
helper.updateEmptyMessage(llEmpty);
helper.updateHeader(llButtonsHeader);
hintManager.showHintIfAppropriate();
}
@Override
public void onPause()
{
listView.getLoader().onPause();
super.onPause();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
{
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.list_habits_fragment, menu);
MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived);
showArchivedItem.setChecked(listView.getShowArchived());
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_add:
showCreateHabitScreen();
return true;
case R.id.action_show_archived:
toggleShowArchived();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void toggleShowArchived()
{
listView.toggleShowArchived();
activity.invalidateOptionsMenu();
}
private void showCreateHabitScreen()
{
BaseDialogFragment frag = new CreateHabitDialogFragment();
frag.show(getFragmentManager(), "editHabit");
}
private void startActionMode()
{
HabitListSelectionCallback callback =
new HabitListSelectionCallback(activity, listView.getLoader());
callback.setSelectedPositions(listView.getSelectedPositions());
callback.setListener(this);
actionMode = activity.startSupportActionMode(callback);
}
private void finishActionMode()
{
if(actionMode != null) actionMode.finish();
}
@Override
public void onActionModeDestroyed(ActionMode mode)
{
actionMode = null;
listView.cancelSelection();
}
@OnClick(R.id.llHint)
public void onClickHint()
{
hintManager.dismissHint();
}
public ProgressBar getProgressBar()
{
return progressBar;
}
public void refresh(Long refreshKey)
{
listView.refreshData(refreshKey);
}
public interface Listener
{
void onHabitClick(Habit habit);
}
private class HabitListViewListener implements HabitListView.Listener
{
@Override
public void onToggleCheckmark(Habit habit, long timestamp)
{
CommandRunner.getInstance().execute(new ToggleRepetitionCommand(habit, timestamp),
habit.getId());
}
@Override
public void onHabitClick(Habit habit)
{
habitClickListener.onHabitClick(habit);
}
@Override
public void onHabitSelectionStart()
{
if(actionMode == null) startActionMode();
}
@Override
public void onHabitSelectionFinish()
{
finishActionMode();
}
@Override
public void onHabitSelectionChange()
{
if(actionMode != null) actionMode.invalidate();
}
@Override
public void onInvalidToggle()
{
activity.showMessage(R.string.long_press_to_toggle);
}
@Override
public void onDatasetChanged()
{
helper.updateEmptyMessage(llEmpty);
}
}
}

@ -1,307 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.views.RingView;
import java.util.GregorianCalendar;
public class ListHabitsHelper
{
private static final int CHECKMARK_LEFT_TO_RIGHT = 0;
private static final int CHECKMARK_RIGHT_TO_LEFT = 1;
private final int lowContrastColor;
private final int mediumContrastColor;
private final Context context;
private final HabitListLoader loader;
public ListHabitsHelper(Context context, HabitListLoader loader)
{
this.context = context;
this.loader = loader;
lowContrastColor = InterfaceUtils.getStyledColor(context, R.attr.lowContrastTextColor);
mediumContrastColor = InterfaceUtils.getStyledColor(context, R.attr.mediumContrastTextColor);
}
public int getButtonCount()
{
float screenWidth = InterfaceUtils.getScreenWidth(context);
float labelWidth = context.getResources().getDimension(R.dimen.habitNameWidth);
float buttonWidth = context.getResources().getDimension(R.dimen.checkmarkWidth);
return Math.max(0, (int) ((screenWidth - labelWidth) / buttonWidth));
}
public int getHabitNameWidth()
{
float screenWidth = InterfaceUtils.getScreenWidth(context);
float buttonWidth = context.getResources().getDimension(R.dimen.checkmarkWidth);
float padding = InterfaceUtils.dpToPixels(context, 15);
return (int) (screenWidth - padding - getButtonCount() * buttonWidth);
}
public void updateCheckmarkButtons(Habit habit, LinearLayout llButtons)
{
int activeColor = getActiveColor(habit);
int m = llButtons.getChildCount();
Long habitId = habit.getId();
int isChecked[] = loader.checkmarks.get(habitId);
for (int i = 0; i < m; i++)
{
int position = i;
if(getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT)
position = m - i - 1;
TextView tvCheck = (TextView) llButtons.getChildAt(position);
tvCheck.setTag(R.string.habit_key, habitId);
tvCheck.setTag(R.string.offset_key, i);
if(isChecked.length > i)
updateCheckmark(activeColor, tvCheck, isChecked[i]);
}
}
public int getActiveColor(Habit habit)
{
int activeColor = ColorUtils.getColor(context, habit.color);
if(habit.isArchived()) activeColor = mediumContrastColor;
return activeColor;
}
public void initializeLabelAndIcon(View itemView)
{
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getHabitNameWidth(),
LinearLayout.LayoutParams.WRAP_CONTENT, 1);
itemView.findViewById(R.id.label).setLayoutParams(params);
}
public void updateNameAndIcon(Habit habit, RingView ring, TextView tvName)
{
int activeColor = getActiveColor(habit);
tvName.setText(habit.name);
tvName.setTextColor(activeColor);
int score = loader.scores.get(habit.getId());
float percentage = (float) score / Score.MAX_VALUE;
ring.setColor(activeColor);
ring.setPercentage(percentage);
ring.setPrecision(1.0f / 16);
}
public void updateCheckmark(int activeColor, TextView tvCheck, int check)
{
switch (check)
{
case 2:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(activeColor);
tvCheck.setTag(R.string.toggle_key, 2);
break;
case 1:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(lowContrastColor);
tvCheck.setTag(R.string.toggle_key, 1);
break;
case 0:
tvCheck.setText(R.string.fa_times);
tvCheck.setTextColor(lowContrastColor);
tvCheck.setTag(R.string.toggle_key, 0);
break;
}
}
public View inflateHabitCard(LayoutInflater inflater,
View.OnLongClickListener onCheckmarkLongClickListener,
View.OnClickListener onCheckmarkClickListener)
{
View view = inflater.inflate(R.layout.list_habits_item, null);
initializeLabelAndIcon(view);
inflateCheckmarkButtons(view, onCheckmarkLongClickListener, onCheckmarkClickListener,
inflater);
return view;
}
public void updateHabitCard(View view, Habit habit, boolean selected)
{
RingView scoreRing = ((RingView) view.findViewById(R.id.scoreRing));
TextView tvName = (TextView) view.findViewById(R.id.label);
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
llInner.setTag(R.string.habit_key, habit.getId());
llInner.setOnTouchListener(new HotspotTouchListener());
updateNameAndIcon(habit, scoreRing, tvName);
updateCheckmarkButtons(habit, llButtons);
updateHabitCardBackground(llInner, selected);
}
public void updateHabitCardBackground(View view, boolean isSelected)
{
if (android.os.Build.VERSION.SDK_INT >= 21)
{
if (isSelected)
view.setBackgroundResource(R.drawable.selected_box);
else
view.setBackgroundResource(R.drawable.ripple);
}
else
{
Drawable background;
if (isSelected)
background = InterfaceUtils.getStyledDrawable(context, R.attr.selectedBackground);
else
background = InterfaceUtils.getStyledDrawable(context, R.attr.cardBackground);
view.setBackgroundDrawable(background);
}
}
public void inflateCheckmarkButtons(View view, View.OnLongClickListener onLongClickListener,
View.OnClickListener onClickListener, LayoutInflater inflater)
{
for (int i = 0; i < getButtonCount(); i++)
{
View check = inflater.inflate(R.layout.list_habits_item_check, null);
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
btCheck.setTypeface(InterfaceUtils.getFontAwesome(context));
btCheck.setOnLongClickListener(onLongClickListener);
btCheck.setOnClickListener(onClickListener);
btCheck.setHapticFeedbackEnabled(false);
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
}
view.setTag(R.id.timestamp_key, DateUtils.getStartOfToday());
}
public void updateHeader(ViewGroup header)
{
LayoutInflater inflater = LayoutInflater.from(context);
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
header.removeAllViews();
for (int i = 0; i < getButtonCount(); i++)
{
int position = 0;
if(getCheckmarkOrder() == CHECKMARK_LEFT_TO_RIGHT)
position = i;
View tvDay = inflater.inflate(R.layout.list_habits_header_check, null);
TextView btCheck = (TextView) tvDay.findViewById(R.id.tvCheck);
btCheck.setText(DateUtils.formatHeaderDate(day));
header.addView(tvDay, position);
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
}
}
public void updateEmptyMessage(View view)
{
if (loader.getLastLoadTimestamp() == null) view.setVisibility(View.GONE);
else view.setVisibility(loader.habits.size() > 0 ? View.GONE : View.VISIBLE);
}
public void toggleCheckmarkView(View v, Habit habit)
{
int androidColor = ColorUtils.getColor(context, habit.color);
if (v.getTag(R.string.toggle_key).equals(2))
updateCheckmark(androidColor, (TextView) v, 0);
else
updateCheckmark(androidColor, (TextView) v, 2);
}
public Long getHabitIdFromCheckmarkView(View v)
{
return (Long) v.getTag(R.string.habit_key);
}
public long getTimestampFromCheckmarkView(View v)
{
Integer offset = (Integer) v.getTag(R.string.offset_key);
return DateUtils.getStartOfDay(DateUtils.getLocalTime() -
offset * DateUtils.millisecondsInOneDay);
}
public void triggerRipple(View v, final float x, final float y)
{
final Drawable background = v.getBackground();
if (android.os.Build.VERSION.SDK_INT >= 21)
background.setHotspot(x, y);
background.setState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled});
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
background.setState(new int[]{});
}
}, 25);
}
private static class HotspotTouchListener implements View.OnTouchListener
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
if (android.os.Build.VERSION.SDK_INT >= 21)
v.getBackground().setHotspot(event.getX(), event.getY());
return false;
}
}
public int getCheckmarkOrder()
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean reverse = prefs.getBoolean("pref_checkmark_reverse_order", false);
return reverse ? CHECKMARK_RIGHT_TO_LEFT : CHECKMARK_LEFT_TO_RIGHT;
}
}

@ -0,0 +1,96 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.support.annotation.NonNull;
import android.view.Menu;
import android.view.MenuItem;
import org.isoron.uhabits.R;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseMenu;
import org.isoron.uhabits.utils.InterfaceUtils;
public class ListHabitsMenu extends BaseMenu
{
@NonNull
private final ListHabitsScreen screen;
private boolean showArchived;
public ListHabitsMenu(@NonNull BaseActivity activity,
@NonNull ListHabitsScreen screen)
{
super(activity);
this.screen = screen;
}
@Override
public void onCreate(@NonNull Menu menu)
{
MenuItem nightModeItem = menu.findItem(R.id.action_night_mode);
nightModeItem.setChecked(InterfaceUtils.isNightMode());
MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived);
showArchivedItem.setChecked(showArchived);
}
@Override
public boolean onItemSelected(@NonNull MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_night_mode:
screen.toggleNightMode();
return true;
case R.id.action_add:
screen.showCreateHabitScreen();
return true;
case R.id.action_faq:
screen.showFAQScreen();
return true;
case R.id.action_about:
screen.showAboutScreen();
return true;
case R.id.action_settings:
screen.showSettingsScreen();
return true;
case R.id.action_show_archived:
showArchived = !showArchived;
screen.getRootView().setShowArchived(showArchived);
invalidate();
return true;
default:
return false;
}
}
@Override
protected int getMenuResourceId()
{
return R.menu.main_activity;
}
}

@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.ModelObservable;
import org.isoron.uhabits.ui.BaseRootView;
import org.isoron.uhabits.ui.habits.list.controllers.HabitCardListController;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.list.model.HintList;
import org.isoron.uhabits.ui.habits.list.views.HabitCardListView;
import org.isoron.uhabits.ui.habits.list.views.HintView;
import org.isoron.uhabits.utils.InterfaceUtils;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ListHabitsRootView extends BaseRootView
implements ModelObservable.Listener
{
@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;
@Nullable
private HabitCardListAdapter listAdapter;
public ListHabitsRootView(@NonNull Context context)
{
super(context);
init();
}
@Override
@NonNull
public ProgressBar getProgressBar()
{
return progressBar;
}
public boolean getShowArchived()
{
if(listAdapter == null) return false;
return listAdapter.getIncludeArchived();
}
@NonNull
@Override
public Toolbar getToolbar()
{
return toolbar;
}
@Override
public int getToolbarColor()
{
return InterfaceUtils.getStyledColor(getContext(), R.attr.colorPrimary);
}
@Override
public void onModelChange()
{
updateEmptyView();
}
public void setShowArchived(boolean showArchived)
{
if(listAdapter == null) return;
listAdapter.setShowArchived(showArchived);
}
private void updateEmptyView()
{
if (listAdapter == null) return;
llEmpty.setVisibility(
listAdapter.getCount() > 0 ? View.GONE : View.VISIBLE);
}
public void setController(@Nullable ListHabitsController controller,
@Nullable ListHabitsSelectionMenu menu)
{
listView.setController(null);
if (controller == null || listAdapter == null) return;
HabitCardListController listController =
new HabitCardListController(listAdapter, listView);
listController.setHabitListener(controller);
listController.setSelectionListener(menu);
listView.setController(listController);
}
public void setListAdapter(@NonNull HabitCardListAdapter listAdapter)
{
if (this.listAdapter != null)
listAdapter.getObservable().removeListener(this);
this.listAdapter = listAdapter;
listView.setAdapter(listAdapter);
listAdapter.setListView(listView);
}
private void init()
{
addView(inflate(getContext(), R.layout.list_habits, null));
ButterKnife.bind(this);
tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
initToolbar();
String hints[] =
getContext().getResources().getStringArray(R.array.hints);
HintList hintList = new HintList(hints);
hintView.setHints(hintList);
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
updateEmptyView();
if (listAdapter != null) listAdapter.getObservable().addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
if (listAdapter != null)
listAdapter.getObservable().removeListener(this);
super.onDetachedFromWindow();
}
}

@ -0,0 +1,236 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import com.android.colorpicker.ColorPickerDialog;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.MainActivity;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseScreen;
import org.isoron.uhabits.ui.about.AboutActivity;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.edit.CreateHabitDialogFragment;
import org.isoron.uhabits.ui.habits.edit.EditHabitDialogFragment;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.ui.intro.IntroActivity;
import org.isoron.uhabits.ui.settings.FilePickerDialog;
import org.isoron.uhabits.ui.settings.SettingsActivity;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.File;
public class ListHabitsScreen extends BaseScreen
{
@Nullable
ListHabitsController controller;
@NonNull
private final ListHabitsRootView rootView;
@NonNull
private final ListHabitsSelectionMenu selectionMenu;
public ListHabitsScreen(@NonNull BaseActivity activity)
{
super(activity);
rootView = new ListHabitsRootView(activity);
setRootView(rootView);
ListHabitsMenu menu = new ListHabitsMenu(activity, this);
selectionMenu = new ListHabitsSelectionMenu(this);
setMenu(menu);
setSelectionMenu(selectionMenu);
HabitCardListAdapter adapter = new HabitCardListAdapter();
rootView.setListAdapter(adapter);
selectionMenu.setAdapter(adapter);
}
@Override
public void onResult(int requestCode, int resultCode, Intent data)
{
if (controller == null) return;
switch (resultCode)
{
case HabitsApplication.RESULT_IMPORT_DATA:
showImportScreen();
break;
case HabitsApplication.RESULT_EXPORT_CSV:
controller.onExportCSV();
break;
case HabitsApplication.RESULT_EXPORT_DB:
controller.onExportDB();
break;
case HabitsApplication.RESULT_BUG_REPORT:
controller.onSendBugReport();
break;
}
}
public void setController(@Nullable ListHabitsController controller)
{
this.controller = controller;
rootView.setController(controller, selectionMenu);
}
public void showAboutScreen()
{
Intent intent = new Intent(activity, AboutActivity.class);
activity.startActivity(intent);
}
public void showColorPicker(Habit habit, OnColorSelectedListener callback)
{
int color = ColorUtils.getColor(activity, habit.color);
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,
ColorUtils.getPalette(activity), color, 4,
ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(c -> {
c = ColorUtils.colorToPaletteIndex(activity, c);
callback.onColorSelected(c);
});
picker.show(activity.getSupportFragmentManager(), "picker");
}
public void showCreateHabitScreen()
{
showDialog(new CreateHabitDialogFragment(), "editHabit");
}
public void showDeleteConfirmationScreen(Callback callback)
{
new AlertDialog.Builder(activity)
.setTitle(R.string.delete_habits)
.setMessage(R.string.delete_habits_message)
.setPositiveButton(android.R.string.yes,
(dialog, which) -> callback.run())
.setNegativeButton(android.R.string.no, null)
.show();
}
public void showEditHabitScreen(Habit habit)
{
BaseDialogFragment frag =
EditHabitDialogFragment.newInstance(habit.getId());
frag.show(activity.getSupportFragmentManager(), "editHabit");
}
public void showFAQScreen()
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(activity.getString(R.string.helpURL)));
activity.startActivity(intent);
}
public void showHabitScreen(@NonNull Habit habit)
{
Intent intent = new Intent(activity, ShowHabitActivity.class);
intent.setData(
Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
activity.startActivity(intent);
}
public void showImportScreen()
{
if (controller == null) return;
File dir = FileUtils.getFilesDir(null);
if (dir == null)
{
showMessage(R.string.could_not_import);
return;
}
FilePickerDialog picker = new FilePickerDialog(activity, dir);
picker.setListener(file -> controller.onImportData(file));
picker.show();
}
public void showIntroScreen()
{
Intent intent = new Intent(activity, IntroActivity.class);
activity.startActivity(intent);
}
public void showSettingsScreen()
{
Intent intent = new Intent(activity, SettingsActivity.class);
activity.startActivityForResult(intent, 0);
}
public void toggleNightMode()
{
if (InterfaceUtils.isNightMode())
InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_LIGHT);
else InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_DARK);
refreshTheme();
}
private void refreshTheme()
{
new Handler().postDelayed(() -> {
Intent intent = new Intent(activity, MainActivity.class);
activity.finish();
activity.overridePendingTransition(android.R.anim.fade_in,
android.R.anim.fade_out);
activity.startActivity(intent);
}, 500); // HACK: Let the menu disappear first
}
interface Callback
{
void run();
}
public interface OnColorSelectedListener
{
void onColorSelected(int color);
}
@NonNull
public ListHabitsRootView getRootView()
{
return rootView;
}
}

@ -0,0 +1,200 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.view.MenuItem;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.BaseSelectionMenu;
import org.isoron.uhabits.ui.habits.list.controllers.HabitCardListController;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import java.util.List;
import javax.inject.Inject;
public class ListHabitsSelectionMenu extends BaseSelectionMenu
implements HabitCardListController.SelectionListener
{
@NonNull
private final ListHabitsScreen screen;
@Inject
CommandRunner commandRunner;
@Nullable
private HabitCardListAdapter adapter;
public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen)
{
this.screen = screen;
HabitsApplication.getComponent().inject(this);
}
@Override
public void onDestroy()
{
if (adapter != null) adapter.clearSelection();
super.onDestroy();
}
@Override
public boolean onItemClicked(@NonNull MenuItem item)
{
if (adapter == null) return false;
List<Habit> selected = adapter.getSelected();
if (selected.isEmpty()) return false;
Habit firstHabit = selected.get(0);
switch (item.getItemId())
{
case R.id.action_edit_habit:
edit(firstHabit);
finish();
return true;
case R.id.action_archive_habit:
archive(selected);
finish();
return true;
case R.id.action_unarchive_habit:
unarchive(selected);
finish();
return true;
case R.id.action_delete:
delete(selected);
return true;
case R.id.action_color:
showColorPicker(selected, firstHabit);
return true;
default:
return false;
}
}
@Override
public boolean onPrepare(@NonNull Menu menu)
{
if (adapter == null) return false;
List<Habit> selected = adapter.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 setAdapter(@Nullable HabitCardListAdapter adapter)
{
if (adapter == null) return;
this.adapter = adapter;
}
private void archive(@NonNull List<Habit> selected)
{
commandRunner.execute(new ArchiveHabitsCommand(selected), null);
}
private void delete(@NonNull List<Habit> selected)
{
screen.showDeleteConfirmationScreen(() -> {
commandRunner.execute(new DeleteHabitsCommand(selected), null);
finish();
});
}
private void edit(@NonNull Habit firstHabit)
{
screen.showEditHabitScreen(firstHabit);
}
@Override
protected int getResourceId()
{
return R.menu.list_habits_selection;
}
private void showColorPicker(@NonNull List<Habit> selected,
@NonNull Habit firstHabit)
{
screen.showColorPicker(firstHabit, color -> {
commandRunner.execute(new ChangeHabitColorCommand(selected, color),
null);
finish();
});
}
private void unarchive(@NonNull List<Habit> selected)
{
commandRunner.execute(new UnarchiveHabitsCommand(selected), null);
}
}

@ -0,0 +1,98 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.controllers;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkButtonView;
import org.isoron.uhabits.utils.Preferences;
import javax.inject.Inject;
public class CheckmarkButtonController
{
@Nullable
private CheckmarkButtonView view;
@Nullable
private Listener listener;
@Inject
Preferences prefs;
@NonNull
private Habit habit;
private long timestamp;
public CheckmarkButtonController(@NonNull Habit habit, long timestamp)
{
this.habit = habit;
this.timestamp = timestamp;
HabitsApplication.getComponent().inject(this);
}
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(Habit habit, long timestamp);
}
}

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.controllers;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.views.HabitCardView;
public class HabitCardController implements HabitCardView.Controller
{
@Nullable
private HabitCardView view;
@Nullable
private Listener listener;
@Override
public void onInvalidToggle()
{
if (listener != null) listener.onInvalidToggle();
}
@Override
public void onToggle(Habit habit, long timestamp)
{
if (view != null) view.triggerRipple(0, 0);
if (listener != null) listener.onToggle(habit, timestamp);
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
public void setView(@Nullable HabitCardView view)
{
this.view = view;
}
public interface Listener extends CheckmarkButtonController.Listener
{
}
}

@ -0,0 +1,317 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.controllers;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.mobeta.android.dslv.DragSortListView;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.list.views.HabitCardListView;
/**
* 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 DragSortListView.DropListener,
DragSortListView.DragListener,
HabitCardListView.Controller
{
private final Mode NORMAL_MODE = new NormalMode();
private final Mode SELECTION_MODE = new SelectionMode();
@NonNull
private final HabitCardListAdapter adapter;
@NonNull
private final HabitCardListView view;
@Nullable
private HabitListener habitListener;
@Nullable
private SelectionListener selectionListener;
@NonNull
private Mode activeMode;
public HabitCardListController(@NonNull HabitCardListAdapter adapter,
@NonNull HabitCardListView view)
{
this.adapter = adapter;
this.view = view;
this.activeMode = new NormalMode();
}
/**
* Called when the user is dragging a habit which was originally at position
* 'from' and is currently hovering over position 'to'. Note that the user
* has not yet finished the dragging operation.
*
* @param from the original position of the habit
* @param to the position where the habit is currently hovering
*/
@Override
public void drag(int from, int to)
{
// ignored
}
/**
* 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.reorder(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 user wants to toggle a checkmark.
*
* @param habit the habit of the checkmark
* @param timestamp the timestamps of the checkmark
*/
@Override
public void onToggle(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);
}
/**
* Marks all items as not selected and finishes the selection operation.
*/
private void cancelSelection()
{
adapter.clearSelection();
view.setDragEnabled(true);
activeMode = new NormalMode();
if (selectionListener != null) selectionListener.onSelectionFinish();
}
/**
* 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;
}
public interface HabitListener extends CheckmarkButtonController.Listener
{
/**
* Called when the user clicks a habit.
*
* @param habit the habit clicked
*/
void onHabitClick(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(Habit from, 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.
* <p>
* 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 (habitListener == null) return;
if (activeMode == SELECTION_MODE)
selectionListener.onSelectionChange();
else
selectionListener.onSelectionFinish();
}
}
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains controllers that are specific for ListHabitsActivity
*/
package org.isoron.uhabits.ui.habits.list.controllers;

@ -0,0 +1,222 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.ModelObservable;
import org.isoron.uhabits.ui.habits.list.views.HabitCardListView;
import org.isoron.uhabits.ui.habits.list.views.HabitCardView;
import java.util.LinkedList;
import java.util.List;
import javax.inject.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.
*/
public class HabitCardListAdapter extends BaseAdapter
implements HabitCardListCache.Listener
{
@NonNull
private ModelObservable observable;
@Inject
@NonNull
HabitCardListCache cache;
@Nullable
private HabitCardListView listView;
@NonNull
private final LinkedList<Habit> selected;
public HabitCardListAdapter()
{
this.selected = new LinkedList<>();
this.observable = new ModelObservable();
HabitsApplication.getComponent().inject(this);
cache.setListener(this);
cache.setCheckmarkCount(5); // TODO: make this dynamic somehow
}
/**
* Sets all items as not selected.
*/
public void clearSelection()
{
selected.clear();
notifyDataSetChanged();
}
@Override
public int getCount()
{
return cache.getHabitCount();
}
public boolean getIncludeArchived()
{
return cache.getIncludeArchived();
}
/**
* 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
*/
@Override
@NonNull
public Habit getItem(int position)
{
return cache.getHabitByPosition(position);
}
@Override
public long getItemId(int position)
{
return getItem(position).getId();
}
@NonNull
public ModelObservable getObservable()
{
return observable;
}
@NonNull
public List<Habit> getSelected()
{
return new LinkedList<>(selected);
}
@Override
public View getView(int position,
@Nullable View view,
@Nullable ViewGroup parent)
{
if (listView == null) return null;
Habit habit = cache.getHabitByPosition(position);
int score = cache.getScore(habit.getId());
int checkmarks[] = cache.getCheckmarks(habit.getId());
boolean selected = this.selected.contains(habit);
return listView.buildCardView((HabitCardView) view, habit, score,
checkmarks, 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 onCacheRefresh()
{
notifyDataSetChanged();
observable.notifyListeners();
}
/**
* Notify the adapter that it has been detached from a ListView.
*/
public void onDetached()
{
cache.onDetached();
}
/**
* Changes the order of habits on the adapter.
* <p>
* 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 reorder(int from, int to)
{
cache.reorder(from, to);
cache.refreshAllHabits(false);
notifyDataSetChanged();
}
/**
* Sets the HabitCardListView that this adapter will provide data for.
* <p>
* 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();
}
public void setShowArchived(boolean showArchived)
{
cache.setIncludeArchived(showArchived);
cache.refreshAllHabits(true);
}
}

@ -0,0 +1,334 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DateUtils;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
/**
* A HabitCardListCache fetches and keeps a cache of all the data necessary to
* render a HabitCardListView.
* <p>
* 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.
*/
public class HabitCardListCache implements CommandRunner.Listener
{
boolean includeArchived;
private int checkmarkCount;
private BaseTask currentFetchTask;
@Nullable
private Listener listener;
@Nullable
private Long lastLoadTimestamp;
@NonNull
private CacheData data;
@Inject
CommandRunner commandRunner;
public HabitCardListCache()
{
data = new CacheData();
HabitsApplication.getComponent().inject(this);
}
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.habitsList.get(position);
}
public int getHabitCount()
{
return data.habits.size();
}
@Nullable
public Long getLastLoadTimestamp()
{
return lastLoadTimestamp;
}
public int getScore(long habitId)
{
return data.scores.get(habitId);
}
public boolean getIncludeArchived()
{
return includeArchived;
}
public void onAttached()
{
// refreshAllHabits(true);
if (lastLoadTimestamp == null) refreshAllHabits(true);
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (refreshKey == null) refreshAllHabits(true);
else refreshHabit(refreshKey);
}
public void onDetached()
{
// commandRunner.removeListener(this);
}
public void refreshAllHabits(final boolean refreshScoresAndCheckmarks)
{
if (currentFetchTask != null) currentFetchTask.cancel(true);
currentFetchTask = new RefreshAllHabitsTask(refreshScoresAndCheckmarks);
currentFetchTask.execute();
}
public void refreshHabit(final Long id)
{
new RefreshHabitTask(id).execute();
}
public void reorder(int from, int to)
{
Habit fromHabit = data.habitsList.get(from);
Habit toHabit = data.habitsList.get(to);
data.habitsList.remove(from);
data.habitsList.add(to, fromHabit);
Habit.reorder(fromHabit, toHabit);
}
public void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setIncludeArchived(boolean includeArchived)
{
this.includeArchived = includeArchived;
}
public void setListener(@Nullable 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
{
/**
* Called when the data on the cache has been modified.
*/
void onCacheRefresh();
}
private class CacheData
{
@NonNull
public HashMap<Long, Habit> habits;
@NonNull
public List<Habit> habitsList;
@NonNull
public HashMap<Long, int[]> checkmarks;
@NonNull
public HashMap<Long, Integer> scores;
/**
* Creates a new CacheData without any content.
*/
public CacheData()
{
habits = new HashMap<>();
habitsList = new LinkedList<>();
checkmarks = new HashMap<>();
scores = new HashMap<>();
}
public void copyCheckmarksFrom(@NonNull CacheData oldData)
{
int[] empty = new int[checkmarkCount];
for (Long id : habits.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 : habits.keySet())
{
if (oldData.scores.containsKey(id))
scores.put(id, oldData.scores.get(id));
else scores.put(id, 0);
}
}
public void fetchHabits()
{
habitsList = Habit.getAll(includeArchived);
for (Habit h : habitsList)
habits.put(h.getId(), h);
}
}
private class RefreshAllHabitsTask extends BaseTask
{
@NonNull
private CacheData newData;
private final boolean refreshScoresAndCheckmarks;
public RefreshAllHabitsTask(boolean refreshScoresAndCheckmarks)
{
this.refreshScoresAndCheckmarks = refreshScoresAndCheckmarks;
newData = new CacheData();
}
private void commit()
{
data = newData;
}
@Override
protected void doInBackground()
{
newData.fetchHabits();
newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data);
commit();
if (!refreshScoresAndCheckmarks) return;
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom =
dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay;
int current = 0;
for (Habit h : newData.habitsList)
{
if (isCancelled()) return;
Long id = h.getId();
newData.scores.put(id, h.scores.getTodayValue());
newData.checkmarks.put(id,
h.checkmarks.getValues(dateFrom, dateTo));
publishProgress(current++, newData.habits.size());
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if (isCancelled()) return;
lastLoadTimestamp = DateUtils.getStartOfToday();
currentFetchTask = null;
if (listener != null) listener.onCacheRefresh();
super.onPostExecute(null);
}
@Override
protected void onProgressUpdate(Integer... values)
{
if (listener != null) listener.onCacheRefresh();
}
}
private class RefreshHabitTask extends BaseTask
{
private final Long id;
public RefreshHabitTask(Long id)
{
this.id = id;
}
@Override
protected void doInBackground()
{
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom =
dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay;
Habit h = Habit.get(id);
if (h == null) return;
data.habits.put(id, h);
data.scores.put(id, h.scores.getTodayValue());
data.checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
}
@Override
protected void onPostExecute(Void aVoid)
{
if (listener != null) listener.onCacheRefresh();
super.onPostExecute(null);
}
}
}

@ -0,0 +1,81 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.model;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.Preferences;
import javax.inject.Inject;
/**
* Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown.
*/
public class HintList
{
@Inject
Preferences prefs;
@NonNull
private final String[] hints;
/**
* Constructs a new list containing the provided hints.
*
* @param hints initial list of hints
*/
public HintList(@NonNull String hints[])
{
this.hints = hints;
HabitsApplication.getComponent().inject(this);
}
/**
* Returns a new hint to be shown to the user.
* <p>
* The hint returned is marked as read on the list, and will not be returned
* 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);
}
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains model classes that are specific for ListHabitsActivity
*/
package org.isoron.uhabits.ui.habits.list.model;

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains classes for ListHabitsActivity.
*/
package org.isoron.uhabits.ui.habits.list;

@ -0,0 +1,116 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.views;
import android.content.Context;
import android.graphics.Canvas;
import android.view.HapticFeedbackConstants;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.utils.InterfaceUtils;
import butterknife.BindView;
import butterknife.ButterKnife;
public class CheckmarkButtonView extends FrameLayout
{
private int color;
private int value;
@BindView(R.id.tvCheck)
TextView tvCheck;
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;
postInvalidate();
}
public void toggle()
{
value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED :
Checkmark.CHECKED_EXPLICITLY);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
postInvalidate();
}
private void init()
{
addView(
inflate(getContext(), R.layout.list_habits_card_checkmark, null));
ButterKnife.bind(this);
setWillNotDraw(false);
setHapticFeedbackEnabled(false);
tvCheck.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
}
@Override
protected void onDraw(Canvas canvas)
{
int lowContrastColor = InterfaceUtils.getStyledColor(getContext(),
R.attr.lowContrastTextColor);
if (value == Checkmark.CHECKED_EXPLICITLY)
{
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(color);
}
if (value == Checkmark.CHECKED_IMPLICITLY)
{
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(lowContrastColor);
}
if (value == Checkmark.UNCHECKED)
{
tvCheck.setText(R.string.fa_times);
tvCheck.setTextColor(lowContrastColor);
}
super.onDraw(canvas);
}
}

@ -0,0 +1,190 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.views;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.Preferences;
import javax.inject.Inject;
public class CheckmarkPanelView extends LinearLayout
{
private static final int CHECKMARK_LEFT_TO_RIGHT = 0;
private static final int CHECKMARK_RIGHT_TO_LEFT = 1;
@Inject
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 CheckmarkPanelView(Context context,
AttributeSet attrs,
int defStyleAttr)
{
super(context, attrs, defStyleAttr);
init();
}
public CheckmarkButtonView getButton(int position)
{
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;
}
public void setHabit(@NonNull Habit habit)
{
this.habit = habit;
}
private void addCheckmarkButtons()
{
removeAllViews();
for (int i = 0; i < nButtons; i++)
addView(new CheckmarkButtonView(getContext()));
}
private int getCheckmarkOrder()
{
if (isInEditMode()) return CHECKMARK_LEFT_TO_RIGHT;
return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT :
CHECKMARK_LEFT_TO_RIGHT;
}
private CheckmarkButtonView indexToButton(int i)
{
int position = i;
if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT)
position = nButtons - i - 1;
return (CheckmarkButtonView) getChildAt(position);
}
private void init()
{
if (isInEditMode()) return;
HabitsApplication.getComponent().inject(this);
setWillNotDraw(false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth);
float buttonHeight =
getResources().getDimension(R.dimen.checkmarkHeight);
float width = buttonWidth * nButtons;
widthMeasureSpec =
MeasureSpec.makeMeasureSpec((int) width, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) buttonHeight,
MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void setupButtonControllers(long timestamp,
CheckmarkButtonView buttonView)
{
if (controller == null) return;
CheckmarkButtonController buttonController =
new CheckmarkButtonController(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;
}
}
public interface Controller extends CheckmarkButtonController.Listener
{
}
}

@ -0,0 +1,165 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.views;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ListAdapter;
import com.mobeta.android.dslv.DragSortController;
import com.mobeta.android.dslv.DragSortListView;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.controllers.HabitCardController;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
public class HabitCardListView extends DragSortListView
{
@Nullable
private HabitCardListAdapter adapter;
@Nullable
private Controller controller;
public HabitCardListView(Context context, AttributeSet attrs)
{
super(context, attrs);
setFloatViewManager(new ViewManager());
setDragEnabled(true);
setLongClickable(true);
}
/**
* Builds a new HabitCardView to be eventually added to this list,
* containing the given data.
*
* @param cardView an old HabitCardView that should be reused if possible,
* possibly null
* @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 buildCardView(@Nullable HabitCardView cardView,
@NonNull Habit habit,
int score,
int[] checkmarks,
boolean selected)
{
if (cardView == null) cardView = new HabitCardView(getContext());
cardView.setHabit(habit);
cardView.setSelected(selected);
cardView.setCheckmarkValues(checkmarks);
cardView.setScore(score);
if (controller != null)
{
HabitCardController cardController = new HabitCardController();
cardController.setListener(controller);
cardView.setController(cardController);
cardController.setView(cardView);
}
return cardView;
}
@Override
public void setAdapter(ListAdapter adapter)
{
this.adapter = (HabitCardListAdapter) adapter;
super.setAdapter(adapter);
}
public void setController(@Nullable Controller controller)
{
this.controller = controller;
setDropListener(controller);
setDragListener(controller);
setOnItemClickListener(null);
setOnLongClickListener(null);
if (controller == null) return;
setOnItemClickListener((p, v, pos, id) -> controller.onItemClick(pos));
setOnItemLongClickListener((p, v, pos, id) -> {
controller.onItemLongClick(pos);
return true;
});
}
public void toggleShowArchived()
{
// showArchived = !showArchived;
// cache.setIncludeArchived(showArchived);
// cache.refreshAllHabits(true);
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (adapter != null) adapter.onAttached();
}
@Override
protected void onDetachedFromWindow()
{
if (adapter != null) adapter.onDetached();
super.onDetachedFromWindow();
}
public interface Controller extends CheckmarkButtonController.Listener,
HabitCardController.Listener,
DropListener,
DragListener
{
void onItemClick(int pos);
void onItemLongClick(int pos);
}
private class ViewManager extends DragSortController
{
public ViewManager()
{
super(HabitCardListView.this);
setRemoveEnabled(false);
}
@Override
public View onCreateFloatView(int position)
{
if (adapter == null) return null;
return adapter.getView(position, null, null);
}
@Override
public void onDestroyFloatView(View floatView)
{
}
}
}

@ -0,0 +1,216 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.views.RingView;
import java.util.Random;
import butterknife.BindView;
import butterknife.ButterKnife;
import static org.isoron.uhabits.utils.InterfaceUtils.getStyledColor;
import static org.isoron.uhabits.utils.InterfaceUtils.getStyledDrawable;
public class HabitCardView extends FrameLayout
{
private Habit habit;
@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();
public HabitCardView(Context context)
{
super(context);
init();
}
public HabitCardView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public HabitCardView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
init();
}
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(Habit habit)
{
this.habit = habit;
int color = getActiveColor(habit);
label.setText(habit.name);
label.setTextColor(color);
scoreRing.setColor(color);
checkmarkPanel.setColor(color);
checkmarkPanel.setHabit(habit);
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(final float x, final float y)
{
final Drawable background = innerFrame.getBackground();
if (android.os.Build.VERSION.SDK_INT >= 21) 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 int getActiveColor(Habit habit)
{
int mediumContrastColor =
getStyledColor(context, R.attr.mediumContrastTextColor);
int activeColor = ColorUtils.getColor(context, habit.color);
if (habit.isArchived()) activeColor = mediumContrastColor;
return activeColor;
}
private void init()
{
setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
inflate(context, R.layout.list_habits_card, this);
ButterKnife.bind(this);
innerFrame.setOnTouchListener((v, event) -> {
if (android.os.Build.VERSION.SDK_INT >= 21)
v.getBackground().setHotspot(event.getX(), event.getY());
return false;
});
if (isInEditMode()) initEditMode();
}
@SuppressLint("SetTextI18n")
private void initEditMode()
{
String habits[] = {
"Wake up early",
"Wash dishes",
"Exercise",
"Meditate",
"Play guitar",
"Wash clothes",
"Get a haircut"
};
Random rand = new Random();
int color = ColorUtils.CSV_PALETTE[rand.nextInt(10)];
int[] values = {
rand.nextInt(3),
rand.nextInt(3),
rand.nextInt(3),
rand.nextInt(3),
rand.nextInt(3)
};
label.setText(habits[rand.nextInt(habits.length)]);
label.setTextColor(color);
scoreRing.setColor(color);
scoreRing.setPercentage(rand.nextFloat());
checkmarkPanel.setColor(color);
checkmarkPanel.setCheckmarkValues(values);
}
private void updateBackground(boolean isSelected)
{
if (android.os.Build.VERSION.SDK_INT >= 21)
{
if (isSelected)
innerFrame.setBackgroundResource(R.drawable.selected_box);
else innerFrame.setBackgroundResource(R.drawable.ripple);
}
else
{
Drawable background;
if (isSelected) background =
getStyledDrawable(context, R.attr.selectedBackground);
else background = getStyledDrawable(context, R.attr.cardBackground);
innerFrame.setBackgroundDrawable(background);
}
}
public interface Controller extends CheckmarkPanelView.Controller
{
}
}

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.habits.list.views;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DateUtils;
import java.util.GregorianCalendar;
public class HeaderView extends LinearLayout
{
private static final int CHECKMARK_LEFT_TO_RIGHT = 0;
private static final int CHECKMARK_RIGHT_TO_LEFT = 1;
private final Context context;
public HeaderView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.context = context;
}
private void createButtons()
{
removeAllViews();
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < getButtonCount(); i++)
{
int position = 0;
if (getCheckmarkOrder() == CHECKMARK_LEFT_TO_RIGHT) position = i;
View tvDay =
inflate(context, R.layout.list_habits_header_checkmark, null);
TextView btCheck = (TextView) tvDay.findViewById(R.id.tvCheck);
btCheck.setText(DateUtils.formatHeaderDate(day));
addView(tvDay, position);
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
}
}
private int getButtonCount()
{
float labelWidth = getResources().getDimension(R.dimen.habitNameWidth);
float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth);
return Math.max(0,
(int) ((getMeasuredWidth() - labelWidth) / buttonWidth));
}
private int getCheckmarkOrder()
{
if (isInEditMode()) return CHECKMARK_LEFT_TO_RIGHT;
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
boolean reverse =
prefs.getBoolean("pref_checkmark_reverse_order", false);
return reverse ? CHECKMARK_RIGHT_TO_LEFT : CHECKMARK_LEFT_TO_RIGHT;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
createButtons();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

@ -0,0 +1,138 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui.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.ui.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();
}
public HintView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
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 cant taste sweetness."
};
int k = new Random().nextInt(hints.length);
hintContent.setText(hints[k]);
setVisibility(VISIBLE);
setAlpha(1.0f);
}
private void showNext()
{
if (hintList == null) return;
if (!hintList.shouldShow()) return;
String hint = hintList.pop();
if (hint == null) return;
hintContent.setText(hint);
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);
}
}
}

@ -25,10 +25,13 @@ import android.os.Bundle;
import android.support.v7.app.ActionBar;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.BaseActivity;
/**
* 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 Habit habit;
@ -42,7 +45,7 @@ public class ShowHabitActivity extends BaseActivity
habit = Habit.get(ContentUris.parseId(data));
setContentView(R.layout.show_habit_activity);
setupSupportActionBar(true);
// setupSupportActionBar(true);
setupHabitActionBar();
}
@ -54,7 +57,7 @@ public class ShowHabitActivity extends BaseActivity
if (actionBar == null) return;
actionBar.setTitle(habit.name);
setupActionBarColor(ColorUtils.getColor(this, habit.color));
// setupActionBarColor(ColorUtils.getColor(this, habit.color));
}
public Habit getHabit()

@ -52,29 +52,47 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class ShowHabitFragment extends Fragment implements ModelObservable.Listener
public class ShowHabitFragment extends Fragment
implements ModelObservable.Listener
{
Habit habit;
float todayScore;
float lastMonthScore;
float lastYearScore;
int activeColor;
int inactiveColor;
int previousScoreInterval;
private ShowHabitHelper helper;
protected ShowHabitActivity activity;
private List<HabitDataView> dataViews;
@BindView(R.id.sStrengthInterval) Spinner sStrengthInterval;
@BindView(R.id.scoreView) HabitScoreView habitScoreView;
@BindView(R.id.historyView) HabitHistoryView habitHistoryView;
@BindView(R.id.punchcardView) HabitFrequencyView habitFrequencyView;
@BindView(R.id.streakView) HabitStreakView habitStreakView;
@BindView(R.id.sStrengthInterval)
Spinner sStrengthInterval;
@BindView(R.id.scoreView)
HabitScoreView habitScoreView;
@BindView(R.id.historyView)
HabitHistoryView habitHistoryView;
@BindView(R.id.punchcardView)
HabitFrequencyView habitFrequencyView;
@BindView(R.id.streakView)
HabitStreakView habitStreakView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.show_habit, container, false);
@ -87,12 +105,14 @@ public class ShowHabitFragment extends Fragment implements ModelObservable.Liste
helper.updateColors();
helper.updateMainHeader(view);
int defaultScoreInterval = InterfaceUtils.getDefaultScoreInterval(getContext());
int defaultScoreInterval =
InterfaceUtils.getDefaultScoreInterval(getContext());
previousScoreInterval = defaultScoreInterval;
setScoreBucketSize(defaultScoreInterval);
sStrengthInterval.setSelection(defaultScoreInterval);
sStrengthInterval.setOnItemSelectedListener(new OnItemSelectedListener());
sStrengthInterval.setOnItemSelectedListener(
new OnItemSelectedListener());
createDataViews();
helper.updateCardHeaders(view);
@ -131,7 +151,7 @@ public class ShowHabitFragment extends Fragment implements ModelObservable.Liste
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
{
inflater.inflate(R.menu.show_habit_fragment, menu);
// inflater.inflate(R.menu.show_habit_fragment, menu);
}
@Override
@ -147,7 +167,8 @@ public class ShowHabitFragment extends Fragment implements ModelObservable.Liste
{
if (habit == null) return false;
BaseDialogFragment frag = EditHabitDialogFragment.newInstance(habit.getId());
BaseDialogFragment frag =
EditHabitDialogFragment.newInstance(habit.getId());
frag.show(getFragmentManager(), "editHabit");
return true;
}
@ -161,10 +182,10 @@ public class ShowHabitFragment extends Fragment implements ModelObservable.Liste
{
if (getView() == null) return;
habitScoreView.setBucketSize(HabitScoreView.DEFAULT_BUCKET_SIZES[position]);
habitScoreView.setBucketSize(
HabitScoreView.DEFAULT_BUCKET_SIZES[position]);
if(position != previousScoreInterval)
refreshData();
if (position != previousScoreInterval) refreshData();
InterfaceUtils.setDefaultScoreInterval(getContext(), position);
previousScoreInterval = position;
@ -226,10 +247,14 @@ public class ShowHabitFragment extends Fragment implements ModelObservable.Liste
}
}
private class OnItemSelectedListener implements AdapterView.OnItemSelectedListener
private class OnItemSelectedListener
implements AdapterView.OnItemSelectedListener
{
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
public void onItemSelected(AdapterView<?> parent,
View view,
int position,
long id)
{
setScoreBucketSize(position);
}

@ -47,12 +47,14 @@ public class ShowHabitHelper
Integer freqNum = fragment.habit.freqNum;
Integer freqDen = fragment.habit.freqDen;
if (freqNum.equals(freqDen)) return resources.getString(R.string.every_day);
if (freqNum.equals(freqDen))
return resources.getString(R.string.every_day);
if (freqNum == 1)
{
if (freqDen == 7) return resources.getString(R.string.every_week);
if (freqDen % 7 == 0) return resources.getString(R.string.every_x_weeks, freqDen / 7);
if (freqDen % 7 == 0)
return resources.getString(R.string.every_x_weeks, freqDen / 7);
return resources.getString(R.string.every_x_days, freqDen);
}
@ -67,48 +69,62 @@ public class ShowHabitHelper
if (view == null) return;
float todayPercentage = fragment.todayScore / Score.MAX_VALUE;
float monthDiff = todayPercentage - (fragment.lastMonthScore / Score.MAX_VALUE);
float yearDiff = todayPercentage - (fragment.lastYearScore / Score.MAX_VALUE);
float monthDiff =
todayPercentage - (fragment.lastMonthScore / Score.MAX_VALUE);
float yearDiff =
todayPercentage - (fragment.lastYearScore / Score.MAX_VALUE);
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
int androidColor = ColorUtils.getColor(fragment.getActivity(), fragment.habit.color);
int androidColor =
ColorUtils.getColor(fragment.getActivity(), fragment.habit.color);
scoreRing.setColor(androidColor);
scoreRing.setPercentage(todayPercentage);
TextView scoreLabel = (TextView) view.findViewById(R.id.scoreLabel);
TextView monthDiffLabel = (TextView) view.findViewById(R.id.monthDiffLabel);
TextView yearDiffLabel = (TextView) view.findViewById(R.id.yearDiffLabel);
TextView monthDiffLabel =
(TextView) view.findViewById(R.id.monthDiffLabel);
TextView yearDiffLabel =
(TextView) view.findViewById(R.id.yearDiffLabel);
scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100));
String minus = "\u2212";
monthDiffLabel.setText(String.format("%s%.0f%%", (monthDiff >= 0 ? "+" : minus),
monthDiffLabel.setText(
String.format("%s%.0f%%", (monthDiff >= 0 ? "+" : minus),
Math.abs(monthDiff) * 100));
yearDiffLabel.setText(
String.format("%s%.0f%%", (yearDiff >= 0 ? "+" : minus), Math.abs(yearDiff) * 100));
String.format("%s%.0f%%", (yearDiff >= 0 ? "+" : minus),
Math.abs(yearDiff) * 100));
monthDiffLabel.setTextColor(monthDiff >= 0 ? fragment.activeColor : fragment.inactiveColor);
yearDiffLabel.setTextColor(yearDiff >= 0 ? fragment.activeColor : fragment.inactiveColor);
monthDiffLabel.setTextColor(
monthDiff >= 0 ? fragment.activeColor : fragment.inactiveColor);
yearDiffLabel.setTextColor(
yearDiff >= 0 ? fragment.activeColor : fragment.inactiveColor);
}
void updateMainHeader(View view)
{
if (fragment.habit == null) return;
TextView questionLabel = (TextView) view.findViewById(R.id.questionLabel);
TextView questionLabel =
(TextView) view.findViewById(R.id.questionLabel);
questionLabel.setTextColor(fragment.activeColor);
questionLabel.setText(fragment.habit.description);
TextView reminderLabel = (TextView) view.findViewById(R.id.reminderLabel);
TextView reminderLabel =
(TextView) view.findViewById(R.id.reminderLabel);
if (fragment.habit.hasReminder()) reminderLabel.setText(
DateUtils.formatTime(fragment.getActivity(), fragment.habit.reminderHour,
fragment.habit.reminderMin));
else reminderLabel.setText(fragment.getResources().getString(R.string.reminder_off));
DateUtils.formatTime(fragment.getActivity(),
fragment.habit.reminderHour, fragment.habit.reminderMin));
else reminderLabel.setText(
fragment.getResources().getString(R.string.reminder_off));
TextView frequencyLabel = (TextView) view.findViewById(R.id.frequencyLabel);
TextView frequencyLabel =
(TextView) view.findViewById(R.id.frequencyLabel);
frequencyLabel.setText(getFreqText());
if (fragment.habit.description.isEmpty()) questionLabel.setVisibility(View.GONE);
if (fragment.habit.description.isEmpty())
questionLabel.setVisibility(View.GONE);
}
void updateCardHeaders(View view)
@ -126,14 +142,17 @@ public class ShowHabitHelper
if (fragment.habit == null || fragment.activity == null) return;
TextView textView = (TextView) view.findViewById(viewId);
int androidColor = ColorUtils.getColor(fragment.activity, fragment.habit.color);
int androidColor =
ColorUtils.getColor(fragment.activity, fragment.habit.color);
textView.setTextColor(androidColor);
}
void updateColors()
{
fragment.activeColor = ColorUtils.getColor(fragment.getContext(), fragment.habit.color);
fragment.inactiveColor = InterfaceUtils.getStyledColor(fragment.getContext(),
fragment.activeColor =
ColorUtils.getColor(fragment.getContext(), fragment.habit.color);
fragment.inactiveColor =
InterfaceUtils.getStyledColor(fragment.getContext(),
R.attr.mediumContrastTextColor);
}
}

@ -27,6 +27,10 @@ import com.github.paolorotolo.appintro.AppIntroFragment;
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

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains classes for the IntroActivity.
*/
package org.isoron.uhabits.ui.intro;

@ -39,8 +39,11 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
private static final String PARENT_DIR = "..";
private final Activity activity;
private ListView list;
private Dialog dialog;
private File currentPath;
public interface OnFileSelectedListener
@ -59,21 +62,24 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
dialog = new Dialog(activity);
dialog.setContentView(list);
dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
dialog
.getWindow()
.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
navigateTo(initialDirectory);
}
@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())
{
@ -142,7 +148,8 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener
{
public FilePickerAdapter(@NonNull String[] fileList)
{
super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList);
super(FilePickerDialog.this.activity,
android.R.layout.simple_list_item_1, fileList);
}
@Override

@ -22,9 +22,12 @@ package org.isoron.uhabits.ui.settings;
import android.os.Bundle;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.utils.InterfaceUtils;
/**
* Activity that allows the user to view and modify the app settings.
*/
public class SettingsActivity extends BaseActivity
{
@Override
@ -32,9 +35,10 @@ public class SettingsActivity extends BaseActivity
{
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
setupSupportActionBar(true);
// setupSupportActionBar(true);
int color = InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor);
setupActionBarColor(color);
int color =
InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor);
// setupActionBarColor(color);
}
}

@ -29,8 +29,8 @@ import android.support.v7.preference.PreferenceFragmentCompat;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ReminderUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.utils.ReminderUtils;
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener
@ -43,10 +43,14 @@ public class SettingsFragment extends PreferenceFragmentCompat
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
setResultOnPreferenceClick("importData", HabitsApplication.RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV", HabitsApplication.RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB", HabitsApplication.RESULT_EXPORT_DB);
setResultOnPreferenceClick("bugReport", HabitsApplication.RESULT_BUG_REPORT);
setResultOnPreferenceClick("importData",
HabitsApplication.RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV",
HabitsApplication.RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB",
HabitsApplication.RESULT_EXPORT_DB);
setResultOnPreferenceClick("bugReport",
HabitsApplication.RESULT_BUG_REPORT);
updateRingtoneDescription();
@ -62,7 +66,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
private void removePreference(String preferenceKey, String categoryKey)
{
PreferenceCategory cat = (PreferenceCategory) findPreference(categoryKey);
PreferenceCategory cat =
(PreferenceCategory) findPreference(categoryKey);
Preference pref = findPreference(preferenceKey);
cat.removePreference(pref);
}
@ -70,7 +75,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
private void setResultOnPreferenceClick(String key, final int result)
{
Preference pref = findPreference(key);
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener()
pref.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener()
{
@Override
public boolean onPreferenceClick(Preference preference)
@ -99,7 +105,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key)
{
BackupManager.dataChanged("org.isoron.uhabits");
}
@ -111,7 +118,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
if (preference.getKey().equals("reminderSound"))
{
ReminderUtils.startRingtonePickerActivity(this, RINGTONE_REQUEST_CODE);
ReminderUtils.startRingtonePickerActivity(this,
RINGTONE_REQUEST_CODE);
return true;
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Contains classes for the SettingsActivity.
*/
package org.isoron.uhabits.ui.settings;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save