Compare commits

...

71 Commits

Author SHA1 Message Date
534e6c2d9d Merge branch 'hotfix/1.7.3' 2017-05-30 09:34:12 -04:00
b6501c9a29 Fix NullPointerException 2017-05-30 09:22:15 -04:00
238a1c724d Bump version and update changelog 2017-05-30 09:06:50 -04:00
34ca9d17a2 Make usage of BundleSavedState more robust 2017-05-30 09:05:05 -04:00
e844390614 Improve performance of repeated getTodayValue calls 2017-05-29 22:22:31 -04:00
5e00d07b73 Merge branch 'hotfix/1.7.2' 2017-05-28 00:41:37 -04:00
28b6ae7014 Apply dark theme on dialogs
Fixes #291
2017-05-28 00:31:48 -04:00
2a1bf5fc2e Update CHANGELOG 2017-05-27 23:38:58 -04:00
ef7483f9dc Make SQLiteHabitList.toList synchronized 2017-05-27 23:37:24 -04:00
bbb9ed8f99 Update translations 2017-05-27 23:30:45 -04:00
c49d576871 Fix crash at startup 2017-05-27 23:29:35 -04:00
bc66ae4f7a Bump version to 1.7.2 2017-05-27 23:28:16 -04:00
fa416adbb9 Merge branch 'hotfix/1.7.1' 2017-05-21 10:20:48 -04:00
8b835b9918 Update CHANGELOG 2017-05-21 10:10:30 -04:00
471c5d341f Update translations 2017-05-21 10:03:13 -04:00
57296745b3 Improve performance of CheckmarkButtonView 2017-04-19 14:33:59 -04:00
140ab34a76 Verify database version before importing 2017-04-11 22:03:16 -04:00
0d6ad26505 Ignore exception when habit is not found 2017-04-11 21:19:03 -04:00
65cc99dbf7 Switch from View.BaseSavedState to Support Library's AbsSavedState
See https://code.google.com/p/android/issues/detail?id=196430
2017-04-11 17:49:55 -04:00
6855ef9d5e Update translations 2017-04-11 17:26:06 -04:00
4c58b084c6 Fix missing dialog title 2017-04-11 17:19:18 -04:00
5c8e522646 Fix header labels for RTL languages such as Arabic 2017-04-11 17:14:25 -04:00
55da0759d4 Bump version 2017-04-11 16:58:35 -04:00
692fe3218f Merge branch 'release/1.7.0' 2017-04-09 18:13:52 -04:00
387930c08d Fix icon size, remove unused icons 2017-04-09 17:37:14 -04:00
6bd31f9607 Add Basque, Catalan, Persian and Romanian languages 2017-04-09 17:16:08 -04:00
9aafe7160c Update translation links 2017-04-09 16:56:04 -04:00
5cc4aac67a Update translations 2017-04-09 16:41:32 -04:00
831421bc98 Ignore CrowdIn configuration files 2017-04-09 16:03:00 -04:00
161d8f2517 Update changelog 2017-03-31 09:12:53 -04:00
bfe4b822b3 Bump version 2017-03-31 08:15:04 -04:00
19e79a8559 Fix failing test 2017-03-20 22:32:53 -04:00
876d4f0ac7 Merge branch 'feature/scroll-header' into dev
Fixes #155
2017-03-20 20:05:26 -04:00
f4f7faf3a4 Fix a few issues with header scrolling 2017-03-20 20:00:10 -04:00
56f2ae57fe Persist scrolling after configuration change 2017-03-20 18:53:48 -04:00
3fe09efe9b Scroll checkmarks 2017-03-19 20:36:16 -04:00
f0de29fbfe Make HeaderView scrollable 2017-03-19 18:31:43 -04:00
324facfffd Re-enable haptic feedback on CheckmarkButtonView 2017-03-19 16:35:57 -04:00
03e58f9ef2 Preserve position of ScrollableChart on configuration changes
Fixes #240
2017-03-18 23:57:54 -04:00
e6c9f7f0c9 Preserve ScrollView position after configuration change
Fixes #239
2017-03-18 22:41:32 -04:00
42bdedb86a Update dependencies 2017-03-18 22:40:38 -04:00
ab0c510fda Rename export action and move to overflow menu 2017-03-18 22:22:07 -04:00
e46fd58664 Fix ImportTest on Nougat 2017-03-17 22:49:52 -04:00
8532bd402e Update APK filename 2017-03-17 22:11:48 -04:00
2c599b18ef Fix ambiguous reference to R 2017-03-17 21:52:51 -04:00
0d78ba4ba9 Update gradle 2017-03-17 21:52:13 -04:00
Janet Do
611dfa00a5 Export data from the statistics screen
Closes #27
2017-02-02 21:31:37 -05:00
Anirudha Agashe
54a195243d Fixed failing build
Replaced static method to obtain context with appropriate dependency
2017-01-18 12:58:56 -06:00
Anirudha Agashe
4fc30fae53 Remove use of static context from HabitsApplication class (#224)
Remove static context from application class. Replace static method to obtain context with appropriate dependency. Remove context from controller. Add auto factory to export db task.
2017-01-09 23:09:05 -05:00
b3fe9c65d2 Refresh data at midnight
Fixes #221
2016-12-31 18:24:26 -05:00
09f1ae8765 Update circle.yml 2016-12-31 12:30:10 -05:00
0a8b763ece Make TimePicker slightly smaller
Fixes #219
2016-12-30 12:58:37 -05:00
edd5f25529 Update circle.yml 2016-12-25 23:56:34 -05:00
d81fdb41dc Remove temp file after importing 2016-12-25 23:50:38 -05:00
02c8810e46 Bump targetSdkVersion to 25 2016-12-25 23:05:47 -05:00
6adf8061d3 Use FileProvider instead of File URIs 2016-12-25 23:05:17 -05:00
d19d57e5df Use Storage Access Framework when importing files 2016-12-25 11:54:08 -05:00
fd82e6c24b Remember last used order 2016-11-24 06:00:02 -05:00
56263efa39 Sort by score 2016-11-24 05:45:08 -05:00
d5eacba303 Disable drag-and-drop when automatically sorting 2016-11-24 05:27:26 -05:00
222261c674 Add order menu 2016-11-23 06:35:07 -05:00
b1a06df7f8 Implement automatic sorting for SQLHabitList 2016-11-23 05:58:46 -05:00
a1fc7dd0d1 Implement automatic sorting for MemoryHabitList 2016-11-04 05:59:34 -04:00
Justin Inácio
10131d5124 Allow intervals larger than 99 days (#202) 2016-11-02 06:13:31 -04:00
aa94959ad2 Merge tag 'v1.6.2' into dev
v1.6.2
2016-10-13 07:09:19 -04:00
967dc2586b Merge branch 'hotfix/1.6.2' 2016-10-13 07:09:11 -04:00
908fd1d6ae Fix NoSuchMethodError on Android 4.1 2016-10-11 06:34:22 -04:00
45fd8a29e1 Merge tag 'v1.6.1' into dev
v1.6.1
2016-10-10 12:10:23 -04:00
4624acd477 Merge branch 'hotfix/1.6.1' 2016-10-10 12:10:14 -04:00
c8cd4fa389 Delete old database cache 2016-10-10 11:39:18 -04:00
8c4fab28aa Merge tag 'v1.6.0' into dev
v1.6.0
2016-10-10 09:54:29 -04:00
153 changed files with 7313 additions and 4969 deletions

2
.gitignore vendored
View File

@@ -20,3 +20,5 @@ captures/
docs/ docs/
gen/ gen/
local.properties local.properties
crowdin.yaml
local

View File

@@ -1,5 +1,37 @@
# Changelog # Changelog
### 1.7.3 (May 30, 2017)
* Improve performance of 'sort by score'
* Other minor bug fixes
### 1.7.2 (May 27, 2017)
* Fix crash at startup
### 1.7.1 (May 21, 2017)
* Fix crash (BadParcelableException)
* Fix layout for RTL languages such as Arabic
* Automatically detect and reject invalid database files
* Add Hebrew translation
### 1.7.0 (Mar 31, 2017)
* Sort habits automatically
* Allow swiping the header to see previous days
* Import backups directly from Google Drive or Dropbox
* Refresh data automatically at midnight
* Other minor bug fixes and enhancements
### 1.6.2 (Oct 13, 2016)
* Fix crash on Android 4.1
### 1.6.1 (Oct 10, 2016)
* Fix a crash at startup when database is corrupted
### 1.6.0 (Oct 10, 2016) ### 1.6.0 (Oct 10, 2016)
* Add option to make notifications sticky * Add option to make notifications sticky

View File

@@ -4,15 +4,15 @@ apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'jacoco' apply plugin: 'jacoco'
android { android {
compileSdkVersion 23 compileSdkVersion 25
buildToolsVersion "23.0.3" buildToolsVersion "25.0.2"
defaultConfig { defaultConfig {
applicationId "org.isoron.uhabits" applicationId "org.isoron.uhabits"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 23 targetSdkVersion 25
buildConfigField "Integer", "databaseVersion", "14" buildConfigField "Integer", "databaseVersion", "15"
buildConfigField "String", "databaseFilename", "\"uhabits.db\"" buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -25,7 +25,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
debug { debug {
testCoverageEnabled = true testCoverageEnabled = false
} }
} }
@@ -53,7 +53,7 @@ dependencies {
androidTestApt 'com.google.dagger:dagger-compiler:2.2' androidTestApt 'com.google.dagger:dagger-compiler:2.2'
androidTestCompile 'com.android.support:support-annotations:23.3.0' androidTestCompile 'com.android.support:support-annotations:25.3.0'
androidTestCompile 'com.android.support.test:rules:0.5' androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.google.auto.factory:auto-factory:1.0-beta3' androidTestCompile 'com.google.auto.factory:auto-factory:1.0-beta3'
@@ -64,10 +64,10 @@ dependencies {
apt 'com.google.dagger:dagger-compiler:2.2' apt 'com.google.dagger:dagger-compiler:2.2'
apt 'com.jakewharton:butterknife-compiler:8.0.1' apt 'com.jakewharton:butterknife-compiler:8.0.1'
compile 'com.android.support:appcompat-v7:23.3.0' compile 'com.android.support:appcompat-v7:25.3.0'
compile 'com.android.support:design:23.3.0' compile 'com.android.support:design:25.3.0'
compile 'com.android.support:preference-v14:23.3.0' compile 'com.android.support:preference-v14:25.3.0'
compile 'com.android.support:support-v4:23.3.0' compile 'com.android.support:support-v4:25.3.0'
compile 'com.getpebble:pebblekit:3.0.0' compile 'com.getpebble:pebblekit:3.0.0'
compile 'com.github.paolorotolo:appintro:3.4.0' compile 'com.github.paolorotolo:appintro:3.4.0'
compile 'com.google.auto.factory:auto-factory:1.0-beta3' compile 'com.google.auto.factory:auto-factory:1.0-beta3'

View File

@@ -24,6 +24,7 @@ import android.content.*;
import android.os.*; import android.os.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.test.*; import android.support.test.*;
import android.util.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;
@@ -31,6 +32,7 @@ import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import java.io.*;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
@@ -63,6 +65,8 @@ public class BaseAndroidTest
protected AndroidTestComponent component; protected AndroidTestComponent component;
protected ModelFactory modelFactory;
@Before @Before
public void setUp() public void setUp()
{ {
@@ -89,7 +93,7 @@ public class BaseAndroidTest
taskRunner = component.getTaskRunner(); taskRunner = component.getTaskRunner();
logger = component.getHabitsLogger(); logger = component.getHabitsLogger();
ModelFactory modelFactory = component.getModelFactory(); modelFactory = component.getModelFactory();
fixtures = new HabitFixtures(modelFactory, habitList); fixtures = new HabitFixtures(modelFactory, habitList);
latch = new CountDownLatch(1); latch = new CountDownLatch(1);
@@ -130,4 +134,18 @@ public class BaseAndroidTest
fail(); fail();
} }
} }
protected void startTracing()
{
File dir = FileUtils.getFilesDir(targetContext, "Profile");
assertNotNull(dir);
String tracePath = dir.getAbsolutePath() + "/performance.trace";
Log.d("PerformanceTest", String.format("Saving trace file to %s", tracePath));
Debug.startMethodTracingSampling(tracePath, 0, 1000);
}
protected void stopTracing()
{
Debug.stopMethodTracing();
}
} }

View File

@@ -225,7 +225,7 @@ public class BaseViewTest extends BaseAndroidTest
throws IOException throws IOException
{ {
File dir = FileUtils.getSDCardDir("test-screenshots"); File dir = FileUtils.getSDCardDir("test-screenshots");
if (dir == null) dir = FileUtils.getFilesDir("test-screenshots"); if (dir == null) dir = FileUtils.getFilesDir(targetContext,"test-screenshots");
if (dir == null) throw new RuntimeException( if (dir == null) throw new RuntimeException(
"Could not find suitable dir for screenshots"); "Could not find suitable dir for screenshots");

View File

@@ -39,12 +39,18 @@ public class HabitFixtures
} }
public Habit createEmptyHabit() public Habit createEmptyHabit()
{
return createEmptyHabit(null);
}
public Habit createEmptyHabit(Long id)
{ {
Habit habit = modelFactory.buildHabit(); Habit habit = modelFactory.buildHabit();
habit.setName("Meditate"); habit.setName("Meditate");
habit.setDescription("Did you meditate this morning?"); habit.setDescription("Did you meditate this morning?");
habit.setColor(3); habit.setColor(3);
habit.setFrequency(Frequency.DAILY); habit.setFrequency(Frequency.DAILY);
habit.setId(id);
habitList.add(habit); habitList.add(habit);
return habit; return habit;
} }

View File

@@ -46,12 +46,12 @@ public class CheckmarkButtonViewTest extends BaseViewTest
public void setUp() public void setUp()
{ {
super.setUp(); super.setUp();
setSimilarityCutoff(0.03f); setSimilarityCutoff(0.015f);
latch = new CountDownLatch(1); latch = new CountDownLatch(1);
view = new CheckmarkButtonView(targetContext); view = new CheckmarkButtonView(targetContext);
view.setValue(Checkmark.UNCHECKED); view.setValue(Checkmark.UNCHECKED);
view.setColor(ColorUtils.getAndroidTestColor(7)); view.setColor(ColorUtils.getAndroidTestColor(5));
measureView(view, dpToPixels(40), dpToPixels(40)); measureView(view, dpToPixels(40), dpToPixels(40));
} }

View File

@@ -60,6 +60,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest
view = new CheckmarkPanelView(targetContext); view = new CheckmarkPanelView(targetContext);
view.setHabit(habit); view.setHabit(habit);
view.setCheckmarkValues(checkmarks); view.setCheckmarkValues(checkmarks);
view.setButtonCount(4);
view.setColor(ColorUtils.getAndroidTestColor(7)); view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(view, dpToPixels(200), dpToPixels(200)); measureView(view, dpToPixels(200), dpToPixels(200));

View File

@@ -40,8 +40,6 @@ import static org.junit.Assert.*;
@MediumTest @MediumTest
public class ImportTest extends BaseAndroidTest public class ImportTest extends BaseAndroidTest
{ {
private File baseDir;
private Context context; private Context context;
@Override @Override
@@ -50,11 +48,8 @@ public class ImportTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
DateUtils.setFixedLocalTime(null); DateUtils.setFixedLocalTime(null);
fixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
context = InstrumentationRegistry.getInstrumentation().getContext(); context = InstrumentationRegistry.getInstrumentation().getContext();
baseDir = FileUtils.getFilesDir("Backups");
if (baseDir == null) fail("baseDir should not be null");
} }
@Test @Test
@@ -149,8 +144,7 @@ public class ImportTest extends BaseAndroidTest
private void importFromFile(String assetFilename) throws IOException private void importFromFile(String assetFilename) throws IOException
{ {
File file = File file = File.createTempFile("asset", "");
new File(String.format("%s/%s", baseDir.getPath(), assetFilename));
copyAssetToFile(assetFilename, file); copyAssetToFile(assetFilename, file);
assertTrue(file.exists()); assertTrue(file.exists());
assertTrue(file.canRead()); assertTrue(file.canRead());
@@ -159,5 +153,7 @@ public class ImportTest extends BaseAndroidTest
assertThat(importer.canHandle(file), is(true)); assertThat(importer.canHandle(file), is(true));
importer.importHabitsFromFile(file); importer.importHabitsFromFile(file);
file.delete();
} }
} }

View File

@@ -0,0 +1,240 @@
/*
* 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.models;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.hamcrest.*;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.isoron.uhabits.models.HabitList.Order.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HabitListTest extends BaseAndroidTest
{
private ArrayList<Habit> habitsArray;
private HabitList activeHabits;
private HabitList reminderHabits;
@Override
public void setUp()
{
super.setUp();
habitList.removeAll();
habitsArray = new ArrayList<>();
for (int i = 0; i < 10; i++)
{
Habit habit = fixtures.createEmptyHabit((long) i);
habitsArray.add(habit);
if (i % 3 == 0)
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
habitList.update(habit);
}
habitsArray.get(0).setArchived(true);
habitsArray.get(1).setArchived(true);
habitsArray.get(4).setArchived(true);
habitsArray.get(7).setArchived(true);
activeHabits = habitList.getFiltered(new HabitMatcherBuilder().build());
reminderHabits = habitList.getFiltered(new HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build());
}
@Test
public void test_size()
{
assertThat(habitList.size(), equalTo(10));
}
@Test
public void test_countActive()
{
assertThat(activeHabits.size(), equalTo(6));
}
@Test
public void test_getByPosition()
{
assertThat(habitList.getByPosition(0), equalTo(habitsArray.get(0)));
assertThat(habitList.getByPosition(3), equalTo(habitsArray.get(3)));
assertThat(habitList.getByPosition(9), equalTo(habitsArray.get(9)));
assertThat(activeHabits.getByPosition(0), equalTo(habitsArray.get(2)));
}
@Test
public void test_getHabitsWithReminder()
{
assertThat(reminderHabits.size(), equalTo(4));
assertThat(reminderHabits.getByPosition(1),
equalTo(habitsArray.get(3)));
}
@Test
public void test_get_withInvalidId()
{
assertThat(habitList.getById(100L), is(nullValue()));
}
@Test
public void test_get_withValidId()
{
Habit habit1 = habitsArray.get(0);
Habit habit2 = habitList.getById(habit1.getId());
assertThat(habit1, equalTo(habit2));
}
@Test
public void test_reorder()
{
int operations[][] = {
{ 5, 2 }, { 3, 7 }, { 4, 4 }, { 3, 2 }
};
int expectedPosition[][] = {
{ 0, 1, 3, 4, 5, 2, 6, 7, 8, 9 },
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
{ 0, 1, 7, 2, 4, 3, 5, 6, 8, 9 },
};
for (int i = 0; i < operations.length; i++)
{
int from = operations[i][0];
int to = operations[i][1];
Habit fromHabit = habitList.getByPosition(from);
Habit toHabit = habitList.getByPosition(to);
habitList.reorder(fromHabit, toHabit);
int actualPositions[] = new int[10];
for (int j = 0; j < 10; j++)
{
Habit h = habitList.getById(j);
assertNotNull(h);
actualPositions[j] = habitList.indexOf(h);
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
@Test
public void test_writeCSV() throws IOException
{
habitList.removeAll();
Habit h1 = fixtures.createEmptyHabit();
h1.setName("Meditate");
h1.setDescription("Did you meditate this morning?");
h1.setFrequency(Frequency.DAILY);
h1.setColor(3);
Habit h2 = fixtures.createEmptyHabit();
h2.setName("Wake up early");
h2.setDescription("Did you wake up before 6am?");
h2.setFrequency(new Frequency(2, 3));
h2.setColor(5);
habitList.update(h1);
habitList.update(h2);
String expectedCSV =
"Position,Name,Description,NumRepetitions,Interval,Color\n" +
"001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
StringWriter writer = new StringWriter();
habitList.writeCSV(writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
}
@Test
public void test_ordering()
{
habitList.removeAll();
Habit h3 = fixtures.createEmptyHabit();
h3.setName("C Habit");
h3.setColor(0);
habitList.update(h3);
Habit h1 = fixtures.createEmptyHabit();
h1.setName("A Habit");
h1.setColor(2);
habitList.update(h1);
Habit h4 = fixtures.createEmptyHabit();
h4.setName("D Habit");
h4.setColor(1);
habitList.update(h4);
Habit h2 = fixtures.createEmptyHabit();
h2.setName("B Habit");
h2.setColor(2);
habitList.update(h2);
habitList.setOrder(BY_POSITION);
assertThat(habitList.getByPosition(0), equalTo(h3));
assertThat(habitList.getByPosition(1), equalTo(h1));
assertThat(habitList.getByPosition(2), equalTo(h4));
assertThat(habitList.getByPosition(3), equalTo(h2));
habitList.setOrder(BY_NAME);
assertThat(habitList.getByPosition(0), equalTo(h1));
assertThat(habitList.getByPosition(1), equalTo(h2));
assertThat(habitList.getByPosition(2), equalTo(h3));
assertThat(habitList.getByPosition(3), equalTo(h4));
habitList.remove(h1);
habitList.add(h1);
assertThat(habitList.getByPosition(0), equalTo(h1));
habitList.setOrder(BY_COLOR);
assertThat(habitList.getByPosition(0), equalTo(h3));
assertThat(habitList.getByPosition(1), equalTo(h4));
assertThat(habitList.getByPosition(2), equalTo(h1));
assertThat(habitList.getByPosition(3), equalTo(h2));
}
}

View File

@@ -125,17 +125,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
assertThat(habits.get(3).getName(), equalTo("habit 3")); assertThat(habits.get(3).getName(), equalTo("habit 3"));
} }
// @Test
// public void testGetAll_withoutArchived()
// {
// List<Habit> habits = habitList.toList();
// assertThat(habits.size(), equalTo(5));
// assertThat(habits.get(3).getName(), equalTo("habit 7"));
//
// List<Habit> another = habitList.toList();
// assertThat(habits, equalTo(another));
// }
@Test @Test
public void testGetById() public void testGetById()
{ {
@@ -178,45 +167,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
assertThat(habitList.indexOf(h2), equalTo(-1)); assertThat(habitList.indexOf(h2), equalTo(-1));
} }
@Test
public void test_reorder()
{
// Same as HabitListTest.java
// TODO: remove duplication
int operations[][] = {
{5, 2}, {3, 7}, {4, 4}, {3, 2}
};
int expectedPosition[][] = {
{0, 1, 3, 4, 5, 2, 6, 7, 8, 9},
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
{0, 1, 7, 2, 4, 3, 5, 6, 8, 9},
};
for (int i = 0; i < operations.length; i++)
{
int from = operations[i][0];
int to = operations[i][1];
Habit fromHabit = habitList.getByPosition(from);
Habit toHabit = habitList.getByPosition(to);
habitList.reorder(fromHabit, toHabit);
int actualPositions[] = new int[10];
for (int j = 0; j < 10; j++)
{
Habit h = habitList.getById(j);
assertNotNull(h);
actualPositions[j] = habitList.indexOf(h);
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
private HabitRecord getRecord(long id) private HabitRecord getRecord(long id)
{ {
return new Select() return new Select()

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2017 Á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.performance;
import android.support.test.filters.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.junit.*;
@MediumTest
public class PerformanceTest extends BaseAndroidTest
{
private Habit habit;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
}
@Test(timeout = 1000)
public void testRepeatedGetTodayValue()
{
for (int i = 0; i < 100000; i++)
{
habit.getScores().getTodayValue();
habit.getCheckmarks().getTodayValue();
}
}
}

View File

@@ -56,7 +56,7 @@ public class ExportCSVTaskTest extends BaseAndroidTest
for (Habit h : habitList) selected.add(h); for (Habit h : habitList) selected.add(h);
taskRunner.execute( taskRunner.execute(
new ExportCSVTask(habitList, selected, archiveFilename -> { new ExportCSVTask(targetContext,habitList, selected, archiveFilename -> {
assertThat(archiveFilename, is(not(nullValue()))); assertThat(archiveFilename, is(not(nullValue())));
File f = new File(archiveFilename); File f = new File(archiveFilename);
assertTrue(f.exists()); assertTrue(f.exists());

View File

@@ -46,7 +46,7 @@ public class ExportDBTaskTest extends BaseAndroidTest
@Test @Test
public void testExportCSV() throws Throwable public void testExportCSV() throws Throwable
{ {
ExportDBTask task = new ExportDBTask(filename -> { ExportDBTask task = new ExportDBTask(targetContext, filename -> {
assertThat(filename, is(not(nullValue()))); assertThat(filename, is(not(nullValue())));
File f = new File(filename); File f = new File(filename);

View File

@@ -21,8 +21,8 @@
<manifest <manifest
package="org.isoron.uhabits" package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="23" android:versionCode="30"
android:versionName="1.6.0"> android:versionName="1.7.3">
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
@@ -223,5 +223,15 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="org.isoron.uhabits"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,3 @@
delete from Score;
delete from Streak;
delete from Checkmarks;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -21,10 +21,10 @@ package org.isoron.uhabits;
import android.app.*; import android.app.*;
import android.content.*; import android.content.*;
import android.support.annotation.*;
import com.activeandroid.*; import com.activeandroid.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.notifications.*; import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;
@@ -38,7 +38,7 @@ import java.io.*;
*/ */
public class HabitsApplication extends Application public class HabitsApplication extends Application
{ {
private static Context context; private Context context;
private static AppComponent component; private static AppComponent component;
@@ -58,26 +58,14 @@ public class HabitsApplication extends Application
HabitsApplication.component = component; HabitsApplication.component = component;
} }
@NonNull
@Deprecated
public static Context getContext()
{
if (context == null) throw new RuntimeException("context is null");
return context;
}
public static boolean isTestMode() public static boolean isTestMode()
{ {
try try
{ {
if (context != null) Class.forName ("org.isoron.uhabits.BaseAndroidTest");
{
String testClass = "org.isoron.uhabits.BaseAndroidTest";
context.getClassLoader().loadClass(testClass);
}
return true; return true;
} }
catch (final Exception e) catch (final ClassNotFoundException e)
{ {
return false; return false;
} }
@@ -87,7 +75,7 @@ public class HabitsApplication extends Application
public void onCreate() public void onCreate()
{ {
super.onCreate(); super.onCreate();
HabitsApplication.context = this; context = this;
component = DaggerAppComponent component = DaggerAppComponent
.builder() .builder()
@@ -96,11 +84,20 @@ public class HabitsApplication extends Application
if (isTestMode()) if (isTestMode())
{ {
File db = DatabaseUtils.getDatabaseFile(); File db = DatabaseUtils.getDatabaseFile(context);
if (db.exists()) db.delete(); if (db.exists()) db.delete();
} }
DatabaseUtils.initializeActiveAndroid(); try
{
DatabaseUtils.initializeActiveAndroid(context);
}
catch (InvalidDatabaseVersionException e)
{
File db = DatabaseUtils.getDatabaseFile(context);
db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
DatabaseUtils.initializeActiveAndroid(context);
}
widgetUpdater = component.getWidgetUpdater(); widgetUpdater = component.getWidgetUpdater();
widgetUpdater.startListening(); widgetUpdater.startListening();
@@ -125,7 +122,7 @@ public class HabitsApplication extends Application
@Override @Override
public void onTerminate() public void onTerminate()
{ {
HabitsApplication.context = null; context = null;
ActiveAndroid.dispose(); ActiveAndroid.dispose();
reminderScheduler.stopListening(); reminderScheduler.stopListening();

View File

@@ -40,6 +40,7 @@ import java.io.*;
import static android.os.Build.VERSION.*; import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*; import static android.os.Build.VERSION_CODES.*;
import static android.support.v4.content.FileProvider.*;
/** /**
* Base class for all screens in the application. * Base class for all screens in the application.
@@ -50,6 +51,8 @@ import static android.os.Build.VERSION_CODES.*;
*/ */
public class BaseScreen public class BaseScreen
{ {
public static final int REQUEST_CREATE_DOCUMENT = 1;
protected BaseActivity activity; protected BaseActivity activity;
@Nullable @Nullable
@@ -230,11 +233,14 @@ public class BaseScreen
public void showSendFileScreen(@NonNull String archiveFilename) public void showSendFileScreen(@NonNull String archiveFilename)
{ {
File file = new File(archiveFilename);
Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file);
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND); intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip"); intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, intent.putExtra(Intent.EXTRA_STREAM, fileUri);
Uri.fromFile(new File(archiveFilename))); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(intent); activity.startActivity(intent);
} }

View File

@@ -70,7 +70,7 @@ public class BaseSystem
if (context == null) throw new RuntimeException( if (context == null) throw new RuntimeException(
"application context should not be null"); "application context should not be null");
File dir = FileUtils.getFilesDir("Logs"); File dir = FileUtils.getFilesDir(context, "Logs");
if (dir == null) throw new IOException("log dir should not be null"); if (dir == null) throw new IOException("log dir should not be null");
File logFile = File logFile =

View File

@@ -25,7 +25,7 @@ import android.support.v7.widget.Toolbar;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.intents.*; import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
@@ -94,6 +94,13 @@ public class AboutRootView extends BaseRootView
getContext().startActivity(intent); getContext().startActivity(intent);
} }
@OnClick(R.id.tvTranslate)
public void onClickTranslate()
{
Intent intent = intents.helpTranslate(getContext());
getContext().startActivity(intent);
}
@OnClick(R.id.tvRate) @OnClick(R.id.tvRate)
public void onClickRate() public void onClickRate()
{ {

View File

@@ -24,7 +24,7 @@ import android.support.v7.app.*;
import com.google.auto.factory.*; import com.google.auto.factory.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import butterknife.*; import butterknife.*;

View File

@@ -78,6 +78,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
{ {
long id = savedInstanceState.getLong("habit", -1); long id = savedInstanceState.getLong("habit", -1);
if (id > 0) this.habit = habitList.getById(id); if (id > 0) this.habit = habitList.getById(id);
historyChart.onRestoreInstanceState(
savedInstanceState.getParcelable("historyChart"));
} }
int padding = int padding =
@@ -129,6 +131,7 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
public void onSaveInstanceState(Bundle outState) public void onSaveInstanceState(Bundle outState)
{ {
outState.putLong("habit", habit.getId()); outState.putLong("habit", habit.getId());
outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
} }
public void setController(@NonNull Controller controller) public void setController(@NonNull Controller controller)

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2017 Á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.activities.common.views;
import android.os.*;
import android.support.v4.os.*;
public class BundleSavedState extends android.support.v4.view.AbsSavedState
{
public static final Parcelable.Creator<BundleSavedState> CREATOR =
ParcelableCompat.newCreator(
new ParcelableCompatCreatorCallbacks<BundleSavedState>()
{
@Override
public BundleSavedState createFromParcel(Parcel source,
ClassLoader loader)
{
return new BundleSavedState(source, loader);
}
@Override
public BundleSavedState[] newArray(int size)
{
return new BundleSavedState[size];
}
});
public final Bundle bundle;
public BundleSavedState(Parcelable superState, Bundle bundle)
{
super(superState);
this.bundle = bundle;
}
public BundleSavedState(Parcel source, ClassLoader loader)
{
super(source, loader);
this.bundle = source.readBundle(loader);
}
@Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeBundle(bundle);
}
}

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.common.views;
import android.animation.*; import android.animation.*;
import android.content.*; import android.content.*;
import android.os.*;
import android.util.*; import android.util.*;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
@@ -32,7 +33,9 @@ public abstract class ScrollableChart extends View
private int dataOffset; private int dataOffset;
private int scrollerBucketSize; private int scrollerBucketSize = 1;
private int direction = 1;
private GestureDetector detector; private GestureDetector detector;
@@ -40,6 +43,10 @@ public abstract class ScrollableChart extends View
private ValueAnimator scrollAnimator; private ValueAnimator scrollAnimator;
private ScrollController scrollController;
private int maxDataOffset = 10000;
public ScrollableChart(Context context) public ScrollableChart(Context context)
{ {
super(context); super(context);
@@ -63,8 +70,7 @@ public abstract class ScrollableChart extends View
if (!scroller.isFinished()) if (!scroller.isFinished())
{ {
scroller.computeScrollOffset(); scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); updateDataOffset();
postInvalidate();
} }
else else
{ {
@@ -85,19 +91,50 @@ public abstract class ScrollableChart extends View
float velocityY) float velocityY)
{ {
scroller.fling(scroller.getCurrX(), scroller.getCurrY(), scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
(int) velocityX / 2, 0, 0, 100000, 0, 0); direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
invalidate(); invalidate();
scrollAnimator.setDuration(scroller.getDuration()); scrollAnimator.setDuration(scroller.getDuration());
scrollAnimator.start(); scrollAnimator.start();
return false; return false;
} }
@Override private int getMaxX()
public void onLongPress(MotionEvent e)
{ {
return maxDataOffset * scrollerBucketSize;
}
@Override
public void onRestoreInstanceState(Parcelable state)
{
if(!(state instanceof BundleSavedState))
{
super.onRestoreInstanceState(state);
return;
}
BundleSavedState bss = (BundleSavedState) state;
int x = bss.bundle.getInt("x");
int y = bss.bundle.getInt("y");
direction = bss.bundle.getInt("direction");
dataOffset = bss.bundle.getInt("dataOffset");
maxDataOffset = bss.bundle.getInt("maxDataOffset");
scroller.startScroll(0, 0, x, y, 0);
scroller.computeScrollOffset();
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
public Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putInt("x", scroller.getCurrX());
bundle.putInt("y", scroller.getCurrY());
bundle.putInt("dataOffset", dataOffset);
bundle.putInt("direction", direction);
bundle.putInt("maxDataOffset", maxDataOffset);
return new BundleSavedState(superState, bundle);
} }
@Override @Override
@@ -111,12 +148,14 @@ public abstract class ScrollableChart extends View
if (parent != null) parent.requestDisallowInterceptTouchEvent(true); if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
} }
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(),
(int) -dx, (int) dy, 0);
scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
postInvalidate();
dx = - direction * dx;
dx = Math.min(dx, getMaxX() - scroller.getCurrX());
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx,
(int) dy, 0);
scroller.computeScrollOffset();
updateDataOffset();
return true; return true;
} }
@@ -138,6 +177,32 @@ public abstract class ScrollableChart extends View
return detector.onTouchEvent(event); return detector.onTouchEvent(event);
} }
public void setDirection(int direction)
{
if (direction != 1 && direction != -1)
throw new IllegalArgumentException();
this.direction = direction;
}
@Override
public void onLongPress(MotionEvent e)
{
}
public void setMaxDataOffset(int maxDataOffset)
{
this.maxDataOffset = maxDataOffset;
this.dataOffset = Math.min(dataOffset, maxDataOffset);
scrollController.onDataOffsetChanged(this.dataOffset);
postInvalidate();
}
public void setScrollController(ScrollController scrollController)
{
this.scrollController = scrollController;
}
public void setScrollerBucketSize(int scrollerBucketSize) public void setScrollerBucketSize(int scrollerBucketSize)
{ {
this.scrollerBucketSize = scrollerBucketSize; this.scrollerBucketSize = scrollerBucketSize;
@@ -149,5 +214,25 @@ public abstract class ScrollableChart extends View
scroller = new Scroller(context, null, true); scroller = new Scroller(context, null, true);
scrollAnimator = ValueAnimator.ofFloat(0, 1); scrollAnimator = ValueAnimator.ofFloat(0, 1);
scrollAnimator.addUpdateListener(this); scrollAnimator.addUpdateListener(this);
scrollController = new ScrollController() {};
}
private void updateDataOffset()
{
int newDataOffset = scroller.getCurrX() / scrollerBucketSize;
newDataOffset = Math.max(0, newDataOffset);
newDataOffset = Math.min(maxDataOffset, newDataOffset);
if (newDataOffset != dataOffset)
{
dataOffset = newDataOffset;
scrollController.onDataOffsetChanged(dataOffset);
postInvalidate();
}
}
public interface ScrollController
{
default void onDataOffsetChanged(int newDataOffset) {}
} }
} }

View File

@@ -28,6 +28,7 @@ import android.view.*;
import com.android.datetimepicker.time.*; import com.android.datetimepicker.time.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.dialogs.*; import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.commands.*; import org.isoron.uhabits.commands.*;
@@ -38,6 +39,8 @@ import java.util.*;
import butterknife.*; import butterknife.*;
import static org.isoron.uhabits.activities.ThemeSwitcher.*;
public abstract class BaseDialog extends AppCompatDialogFragment public abstract class BaseDialog extends AppCompatDialogFragment
{ {
@Nullable @Nullable
@@ -61,6 +64,18 @@ public abstract class BaseDialog extends AppCompatDialogFragment
private ColorPickerDialogFactory colorPickerDialogFactory; private ColorPickerDialogFactory colorPickerDialogFactory;
@Override
public int getTheme()
{
AppComponent component =
((HabitsApplication) getContext().getApplicationContext()).getComponent();
if(component.getPreferences().getTheme() == THEME_LIGHT)
return R.style.DialogWithTitle;
else
return R.style.DarkDialogWithTitle;
}
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) public void onActivityCreated(Bundle savedInstanceState)
{ {

View File

@@ -24,7 +24,7 @@ import android.support.v4.app.*;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;

View File

@@ -25,6 +25,7 @@ import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
/** /**
* Activity that allows the user to see and modify the list of habits. * Activity that allows the user to see and modify the list of habits.
@@ -43,6 +44,8 @@ public class ListHabitsActivity extends BaseActivity
private Preferences prefs; private Preferences prefs;
private MidnightTimer midnightTimer;
public ListHabitsComponent getListHabitsComponent() public ListHabitsComponent getListHabitsComponent()
{ {
return component; return component;
@@ -77,6 +80,8 @@ public class ListHabitsActivity extends BaseActivity
screen.setSelectionMenu(selectionMenu); screen.setSelectionMenu(selectionMenu);
rootView.setController(controller, selectionMenu); rootView.setController(controller, selectionMenu);
midnightTimer = component.getMidnightTimer();
setScreen(screen); setScreen(screen);
controller.onStartup(); controller.onStartup();
} }
@@ -84,6 +89,7 @@ public class ListHabitsActivity extends BaseActivity
@Override @Override
protected void onPause() protected void onPause()
{ {
midnightTimer.onPause();
screen.onDettached(); screen.onDettached();
adapter.cancelRefresh(); adapter.cancelRefresh();
super.onPause(); super.onPause();
@@ -95,6 +101,7 @@ public class ListHabitsActivity extends BaseActivity
adapter.refresh(); adapter.refresh();
screen.onAttached(); screen.onAttached();
rootView.postInvalidate(); rootView.postInvalidate();
midnightTimer.onResume();
if (prefs.getTheme() == ThemeSwitcher.THEME_DARK && if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
prefs.isPureBlackEnabled() != pureBlack) prefs.isPureBlackEnabled() != pureBlack)

View File

@@ -23,13 +23,14 @@ import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.utils.*;
import dagger.*; import dagger.*;
@ActivityScope @ActivityScope
@Component(modules = { ActivityModule.class }, @Component(modules = { ActivityModule.class },
dependencies = { AppComponent.class }) dependencies = { AppComponent.class })
public interface ListHabitsComponent extends ActivityComponent public interface ListHabitsComponent
{ {
CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory();
@@ -44,4 +45,6 @@ public interface ListHabitsComponent extends ActivityComponent
ListHabitsScreen getScreen(); ListHabitsScreen getScreen();
ListHabitsSelectionMenu getSelectionMenu(); ListHabitsSelectionMenu getSelectionMenu();
MidnightTimer getMidnightTimer();
} }

View File

@@ -41,6 +41,7 @@ import javax.inject.*;
public class ListHabitsController public class ListHabitsController
implements HabitCardListController.HabitListener implements HabitCardListController.HabitListener
{ {
@NonNull @NonNull
private final ListHabitsScreen screen; private final ListHabitsScreen screen;
@@ -70,6 +71,8 @@ public class ListHabitsController
private ExportCSVTaskFactory exportCSVFactory; private ExportCSVTaskFactory exportCSVFactory;
private ExportDBTaskFactory exportDBFactory;
@Inject @Inject
public ListHabitsController(@NonNull BaseSystem system, public ListHabitsController(@NonNull BaseSystem system,
@NonNull CommandRunner commandRunner, @NonNull CommandRunner commandRunner,
@@ -82,7 +85,8 @@ public class ListHabitsController
@NonNull WidgetUpdater widgetUpdater, @NonNull WidgetUpdater widgetUpdater,
@NonNull @NonNull
ImportDataTaskFactory importTaskFactory, ImportDataTaskFactory importTaskFactory,
@NonNull ExportCSVTaskFactory exportCSVFactory) @NonNull ExportCSVTaskFactory exportCSVFactory,
@NonNull ExportDBTaskFactory exportDBFactory)
{ {
this.adapter = adapter; this.adapter = adapter;
this.commandRunner = commandRunner; this.commandRunner = commandRunner;
@@ -95,6 +99,7 @@ public class ListHabitsController
this.widgetUpdater = widgetUpdater; this.widgetUpdater = widgetUpdater;
this.importTaskFactory = importTaskFactory; this.importTaskFactory = importTaskFactory;
this.exportCSVFactory = exportCSVFactory; this.exportCSVFactory = exportCSVFactory;
this.exportDBFactory = exportDBFactory;
} }
public void onExportCSV() public void onExportCSV()
@@ -110,7 +115,7 @@ public class ListHabitsController
public void onExportDB() public void onExportDB()
{ {
taskRunner.execute(new ExportDBTask(filename -> { taskRunner.execute(exportDBFactory.create(filename -> {
if (filename != null) screen.showSendFileScreen(filename); if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export); else screen.showMessage(R.string.could_not_export);
})); }));
@@ -128,7 +133,8 @@ public class ListHabitsController
taskRunner.execute(() -> habitList.reorder(from, to)); taskRunner.execute(() -> habitList.reorder(from, to));
} }
public void onImportData(@NonNull File file) public void onImportData(@NonNull File file,
@NonNull OnFinishedListener finishedListener)
{ {
taskRunner.execute(importTaskFactory.create(file, result -> { taskRunner.execute(importTaskFactory.create(file, result -> {
switch (result) switch (result)
@@ -146,6 +152,8 @@ public class ListHabitsController
screen.showMessage(R.string.could_not_import); screen.showMessage(R.string.could_not_import);
break; break;
} }
finishedListener.onFinish();
})); }));
} }
@@ -208,4 +216,9 @@ public class ListHabitsController
prefs.updateLastHint(-1, DateUtils.getStartOfToday()); prefs.updateLastHint(-1, DateUtils.getStartOfToday());
screen.showIntroScreen(); screen.showIntroScreen();
} }
public interface OnFinishedListener
{
void onFinish();
}
} }

View File

@@ -112,6 +112,22 @@ public class ListHabitsMenu extends BaseMenu
invalidate(); invalidate();
return true; return true;
case R.id.actionSortColor:
adapter.setOrder(HabitList.Order.BY_COLOR);
return true;
case R.id.actionSortManual:
adapter.setOrder(HabitList.Order.BY_POSITION);
return true;
case R.id.actionSortName:
adapter.setOrder(HabitList.Order.BY_NAME);
return true;
case R.id.actionSortScore:
adapter.setOrder(HabitList.Order.BY_SCORE);
return true;
default: default:
return false; return false;
} }

View File

@@ -26,8 +26,9 @@ import android.support.v7.widget.Toolbar;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.activities.habits.list.views.*; import org.isoron.uhabits.activities.habits.list.views.*;
@@ -43,7 +44,7 @@ import butterknife.*;
public class ListHabitsRootView extends BaseRootView public class ListHabitsRootView extends BaseRootView
implements ModelObservable.Listener, TaskRunner.Listener implements ModelObservable.Listener, TaskRunner.Listener
{ {
public static final int MAX_CHECKMARK_COUNT = 21; public static final int MAX_CHECKMARK_COUNT = 60;
@BindView(R.id.listView) @BindView(R.id.listView)
HabitCardListView listView; HabitCardListView listView;
@@ -132,6 +133,13 @@ public class ListHabitsRootView extends BaseRootView
listController.setSelectionListener(menu); listController.setSelectionListener(menu);
listView.setController(listController); listView.setController(listController);
menu.setListController(listController); menu.setListController(listController);
header.setScrollController(new ScrollableChart.ScrollController() {
@Override
public void onDataOffsetChanged(int newDataOffset)
{
listView.setDataOffset(newDataOffset);
}
});
} }
@Override @Override
@@ -156,6 +164,7 @@ public class ListHabitsRootView extends BaseRootView
{ {
int count = getCheckmarkCount(); int count = getCheckmarkCount();
header.setButtonCount(count); header.setButtonCount(count);
header.setMaxDataOffset(Math.max(MAX_CHECKMARK_COUNT - count, 0));
listView.setCheckmarkCount(count); listView.setCheckmarkCount(count);
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
} }

View File

@@ -19,7 +19,9 @@
package org.isoron.uhabits.activities.habits.list; package org.isoron.uhabits.activities.habits.list;
import android.app.*;
import android.content.*; import android.content.*;
import android.net.*;
import android.support.annotation.*; import android.support.annotation.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
@@ -31,24 +33,32 @@ import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*; import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*; import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.io.*; import java.io.*;
import javax.inject.*; import javax.inject.*;
import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*;
@ActivityScope @ActivityScope
public class ListHabitsScreen extends BaseScreen public class ListHabitsScreen extends BaseScreen
implements CommandRunner.Listener implements CommandRunner.Listener
{ {
public static final int RESULT_BUG_REPORT = 4; public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_EXPORT_CSV = 2; public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3; public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_REPAIR_DB = 5; public static final int RESULT_REPAIR_DB = 5;
public static final int RESULT_IMPORT_DATA = 1; public static final int REQUEST_OPEN_DOCUMENT = 6;
public static final int REQUEST_SETTINGS = 7;
@Nullable @Nullable
private ListHabitsController controller; private ListHabitsController controller;
@@ -125,6 +135,15 @@ public class ListHabitsScreen extends BaseScreen
@Override @Override
public void onResult(int requestCode, int resultCode, Intent data) public void onResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == REQUEST_OPEN_DOCUMENT)
onOpenDocumentResult(resultCode, data);
if (requestCode == REQUEST_SETTINGS)
onSettingsResult(resultCode);
}
private void onSettingsResult(int resultCode)
{ {
if (controller == null) return; if (controller == null) return;
@@ -152,6 +171,30 @@ public class ListHabitsScreen extends BaseScreen
} }
} }
private void onOpenDocumentResult(int resultCode, Intent data)
{
if (controller == null) return;
if (resultCode != Activity.RESULT_OK) return;
try
{
Uri uri = data.getData();
ContentResolver cr = activity.getContentResolver();
InputStream is = cr.openInputStream(uri);
File cacheDir = activity.getExternalCacheDir();
File tempFile = File.createTempFile("import", "", cacheDir);
FileUtils.copy(is, tempFile);
controller.onImportData(tempFile, () -> tempFile.delete());
}
catch (IOException e)
{
showMessage(R.string.could_not_import);
e.printStackTrace();
}
}
public void setController(@Nullable ListHabitsController controller) public void setController(@Nullable ListHabitsController controller)
{ {
this.controller = controller; this.controller = controller;
@@ -208,6 +251,21 @@ public class ListHabitsScreen extends BaseScreen
} }
public void showImportScreen() public void showImportScreen()
{
if (SDK_INT < KITKAT)
{
showImportScreenPreKitKat();
return;
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
}
public void showImportScreenPreKitKat()
{ {
File dir = dirFinder.findStorageDir(null); File dir = dirFinder.findStorageDir(null);
@@ -220,7 +278,8 @@ public class ListHabitsScreen extends BaseScreen
FilePickerDialog picker = filePickerDialogFactory.create(dir); FilePickerDialog picker = filePickerDialogFactory.create(dir);
if (controller != null) if (controller != null)
picker.setListener(file -> controller.onImportData(file)); picker.setListener(file -> controller.onImportData(file, () -> {}));
activity.showDialog(picker.getDialog()); activity.showDialog(picker.getDialog());
} }
@@ -233,7 +292,7 @@ public class ListHabitsScreen extends BaseScreen
public void showSettingsScreen() public void showSettingsScreen()
{ {
Intent intent = intentFactory.startSettingsActivity(activity); Intent intent = intentFactory.startSettingsActivity(activity);
activity.startActivityForResult(intent, 0); activity.startActivityForResult(intent, REQUEST_SETTINGS);
} }
public void toggleNightMode() public void toggleNightMode()

View File

@@ -27,6 +27,8 @@ import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.*; import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.views.*; import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*; import java.util.*;
@@ -41,7 +43,7 @@ import javax.inject.*;
@ActivityScope @ActivityScope
public class HabitCardListAdapter public class HabitCardListAdapter
extends RecyclerView.Adapter<HabitCardViewHolder> extends RecyclerView.Adapter<HabitCardViewHolder>
implements HabitCardListCache.Listener implements HabitCardListCache.Listener, MidnightTimer.MidnightListener
{ {
@NonNull @NonNull
private ModelObservable observable; private ModelObservable observable;
@@ -55,19 +57,36 @@ public class HabitCardListAdapter
@NonNull @NonNull
private final HabitCardListCache cache; private final HabitCardListCache cache;
@NonNull
private Preferences preferences;
private final MidnightTimer midnightTimer;
@Inject @Inject
public HabitCardListAdapter(@NonNull HabitCardListCache cache) public HabitCardListAdapter(@NonNull HabitCardListCache cache,
@NonNull Preferences preferences,
@NonNull MidnightTimer midnightTimer)
{ {
this.preferences = preferences;
this.selected = new LinkedList<>(); this.selected = new LinkedList<>();
this.observable = new ModelObservable(); this.observable = new ModelObservable();
this.cache = cache; this.cache = cache;
this.midnightTimer = midnightTimer;
cache.setListener(this); cache.setListener(this);
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT); cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
cache.setOrder(preferences.getDefaultOrder());
setHasStableIds(true); setHasStableIds(true);
} }
@Override
public void atMidnight()
{
cache.refreshAllHabits();
}
public void cancelRefresh() public void cancelRefresh()
{ {
cache.cancelTasks(); cache.cancelTasks();
@@ -130,12 +149,18 @@ public class HabitCardListAdapter
return selected.isEmpty(); return selected.isEmpty();
} }
public boolean isSortable()
{
return cache.getOrder() == HabitList.Order.BY_POSITION;
}
/** /**
* Notify the adapter that it has been attached to a ListView. * Notify the adapter that it has been attached to a ListView.
*/ */
public void onAttached() public void onAttached()
{ {
cache.onAttached(); cache.onAttached();
midnightTimer.addListener(this);
} }
@Override @Override
@@ -153,6 +178,20 @@ public class HabitCardListAdapter
listView.bindCardView(holder, habit, score, checkmarks, selected); listView.bindCardView(holder, habit, score, checkmarks, selected);
} }
@Override
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.attachCardView(holder);
}
@Override
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.detachCardView(holder);
}
@Override @Override
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent, public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) int viewType)
@@ -168,6 +207,7 @@ public class HabitCardListAdapter
public void onDetached() public void onDetached()
{ {
cache.onDetached(); cache.onDetached();
midnightTimer.removeListener(this);
} }
@Override @Override
@@ -260,6 +300,12 @@ public class HabitCardListAdapter
this.listView = listView; this.listView = listView;
} }
public void setOrder(HabitList.Order order)
{
cache.setOrder(order);
preferences.setDefaultOrder(order);
}
/** /**
* Selects or deselects the item at a given position. * Selects or deselects the item at a given position.
* *

View File

@@ -107,6 +107,11 @@ public class HabitCardListCache implements CommandRunner.Listener
return data.habits.size(); return data.habits.size();
} }
public HabitList.Order getOrder()
{
return filteredHabits.getOrder();
}
public int getScore(long habitId) public int getScore(long habitId)
{ {
return data.scores.get(habitId); return data.scores.get(habitId);
@@ -180,6 +185,13 @@ public class HabitCardListCache implements CommandRunner.Listener
this.listener = listener; this.listener = listener;
} }
public void setOrder(HabitList.Order order)
{
allHabits.setOrder(order);
filteredHabits.setOrder(order);
refreshAllHabits();
}
/** /**
* Interface definition for a callback to be invoked when the data on the * Interface definition for a callback to be invoked when the data on the
* cache has been modified. * cache has been modified.

View File

@@ -20,21 +20,34 @@
package org.isoron.uhabits.activities.habits.list.views; package org.isoron.uhabits.activities.habits.list.views;
import android.content.*; import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*; import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
public class CheckmarkButtonView extends TextView import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.models.Checkmark.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
public class CheckmarkButtonView extends View
{ {
private int color; private int color;
private int value; private int value;
private StyledResources res; private StyledResources styledRes;
private TextPaint paint;
private int lowContrastColor;
private RectF rect;
public CheckmarkButtonView(Context context) public CheckmarkButtonView(Context context)
{ {
@@ -42,6 +55,21 @@ public class CheckmarkButtonView extends TextView
init(); init();
} }
public CheckmarkButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
{
super(ctx, attrs);
init();
if(ctx == null) throw new IllegalStateException();
if(attrs == null) throw new IllegalStateException();
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(ColorUtils.getAndroidTestColor(paletteColor));
int value = getIntAttribute(ctx, attrs, "value", 0);
setValue(value);
}
public void setColor(int color) public void setColor(int color)
{ {
this.color = color; this.color = color;
@@ -57,55 +85,60 @@ public class CheckmarkButtonView extends TextView
public void setValue(int value) public void setValue(int value)
{ {
this.value = value; this.value = value;
updateText(); postInvalidate();
} }
public void toggle() public void toggle()
{ {
value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED : value = (value == CHECKED_EXPLICITLY ? UNCHECKED : CHECKED_EXPLICITLY);
Checkmark.CHECKED_EXPLICITLY);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
updateText(); postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Resources resources = getResources();
paint.setColor(value == CHECKED_EXPLICITLY ? color : lowContrastColor);
int id = (value == UNCHECKED ? R.string.fa_times : R.string.fa_check);
String label = resources.getString(id);
float em = paint.measureText("m");
rect.set(0, 0, getWidth(), getHeight());
rect.offset(0, 0.4f * em);
canvas.drawText(label, rect.centerX(), rect.centerY(), paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
Resources res = getResources();
int height = res.getDimensionPixelSize(R.dimen.checkmarkHeight);
int width = res.getDimensionPixelSize(R.dimen.checkmarkWidth);
widthMeasureSpec = makeMeasureSpec(width, EXACTLY);
heightMeasureSpec = makeMeasureSpec(height, EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} }
private void init() private void init()
{ {
res = new StyledResources(getContext());
setWillNotDraw(false);
setHapticFeedbackEnabled(false);
setMinHeight(
getResources().getDimensionPixelSize(R.dimen.checkmarkHeight));
setMinWidth(
getResources().getDimensionPixelSize(R.dimen.checkmarkWidth));
setFocusable(false); setFocusable(false);
setGravity(Gravity.CENTER);
setTypeface(InterfaceUtils.getFontAwesome(getContext()));
}
private void updateText() Resources res = getResources();
{ styledRes = new StyledResources(getContext());
int lowContrastColor = res.getColor(R.attr.lowContrastTextColor);
if (value == Checkmark.CHECKED_EXPLICITLY) paint = new TextPaint();
{ paint.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
setText(R.string.fa_check); paint.setAntiAlias(true);
setTextColor(color); paint.setTextAlign(Paint.Align.CENTER);
} paint.setTextSize(res.getDimension(R.dimen.regularTextSize));
if (value == Checkmark.CHECKED_IMPLICITLY) rect = new RectF();
{ color = ColorUtils.getAndroidTestColor(0);
setText(R.string.fa_check); lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
setTextColor(lowContrastColor);
}
if (value == Checkmark.UNCHECKED)
{
setText(R.string.fa_times);
setTextColor(lowContrastColor);
}
} }
} }

View File

@@ -53,6 +53,8 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
@NonNull @NonNull
private Habit habit; private Habit habit;
private int dataOffset;
public CheckmarkPanelView(Context context) public CheckmarkPanelView(Context context)
{ {
super(context); super(context);
@@ -75,19 +77,23 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
return (CheckmarkButtonView) getChildAt(position); return (CheckmarkButtonView) getChildAt(position);
} }
public void setCheckmarkValues(int[] checkmarkValues) public void setButtonCount(int newButtonCount)
{ {
this.checkmarkValues = checkmarkValues; if(nButtons != newButtonCount)
if (this.nButtons != checkmarkValues.length)
{ {
this.nButtons = checkmarkValues.length; nButtons = newButtonCount;
addCheckmarkButtons(); addCheckmarkButtons();
} }
setupCheckmarkButtons(); setupCheckmarkButtons();
} }
public void setCheckmarkValues(int[] checkmarkValues)
{
this.checkmarkValues = checkmarkValues;
setupCheckmarkButtons();
}
public void setColor(int color) public void setColor(int color)
{ {
this.color = color; this.color = color;
@@ -100,6 +106,12 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
setupCheckmarkButtons(); setupCheckmarkButtons();
} }
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
setupCheckmarkButtons();
}
public void setHabit(@NonNull Habit habit) public void setHabit(@NonNull Habit habit)
{ {
this.habit = habit; this.habit = habit;
@@ -170,11 +182,13 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
{ {
long timestamp = DateUtils.getStartOfToday(); long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay; long day = DateUtils.millisecondsInOneDay;
timestamp -= day * dataOffset;
for (int i = 0; i < nButtons; i++) for (int i = 0; i < nButtons; i++)
{ {
CheckmarkButtonView buttonView = indexToButton(i); CheckmarkButtonView buttonView = indexToButton(i);
buttonView.setValue(checkmarkValues[i]); if(i + dataOffset >= checkmarkValues.length) break;
buttonView.setValue(checkmarkValues[i + dataOffset]);
buttonView.setColor(color); buttonView.setColor(color);
setupButtonControllers(timestamp, buttonView); setupButtonControllers(timestamp, buttonView);
timestamp -= day; timestamp -= day;

View File

@@ -20,15 +20,17 @@
package org.isoron.uhabits.activities.habits.list.views; package org.isoron.uhabits.activities.habits.list.views;
import android.content.*; import android.content.*;
import android.os.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.v7.widget.*; import android.support.v7.widget.*;
import android.support.v7.widget.helper.*; import android.support.v7.widget.helper.*;
import android.util.*; import android.util.*;
import android.view.*; import android.view.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.models.*;
import java.util.*; import java.util.*;
@@ -44,6 +46,10 @@ public class HabitCardListView extends RecyclerView
private int checkmarkCount; private int checkmarkCount;
private int dataOffset;
private LinkedList<HabitCardViewHolder> attachedHolders;
public HabitCardListView(Context context, AttributeSet attrs) public HabitCardListView(Context context, AttributeSet attrs)
{ {
super(context, attrs); super(context, attrs);
@@ -54,6 +60,13 @@ public class HabitCardListView extends RecyclerView
TouchHelperCallback callback = new TouchHelperCallback(); TouchHelperCallback callback = new TouchHelperCallback();
touchHelper = new ItemTouchHelper(callback); touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(this); touchHelper.attachToRecyclerView(this);
attachedHolders = new LinkedList<>();
}
public void attachCardView(HabitCardViewHolder holder)
{
attachedHolders.add(holder);
} }
/** /**
@@ -75,13 +88,12 @@ public class HabitCardListView extends RecyclerView
int[] checkmarks, int[] checkmarks,
boolean selected) boolean selected)
{ {
int visibleCheckmarks[] =
Arrays.copyOfRange(checkmarks, 0, checkmarkCount);
HabitCardView cardView = (HabitCardView) holder.itemView; HabitCardView cardView = (HabitCardView) holder.itemView;
cardView.setHabit(habit); cardView.setHabit(habit);
cardView.setSelected(selected); cardView.setSelected(selected);
cardView.setCheckmarkValues(visibleCheckmarks); cardView.setCheckmarkValues(checkmarks);
cardView.setCheckmarkCount(checkmarkCount);
cardView.setDataOffset(dataOffset);
cardView.setScore(score); cardView.setScore(score);
if (controller != null) setupCardViewController(holder); if (controller != null) setupCardViewController(holder);
return cardView; return cardView;
@@ -92,6 +104,11 @@ public class HabitCardListView extends RecyclerView
return new HabitCardView(getContext()); return new HabitCardView(getContext());
} }
public void detachCardView(HabitCardViewHolder holder)
{
attachedHolders.remove(holder);
}
@Override @Override
public void setAdapter(RecyclerView.Adapter adapter) public void setAdapter(RecyclerView.Adapter adapter)
{ {
@@ -109,6 +126,16 @@ public class HabitCardListView extends RecyclerView
this.controller = controller; this.controller = controller;
} }
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
for (HabitCardViewHolder holder : attachedHolders)
{
HabitCardView cardView = (HabitCardView) holder.itemView;
cardView.setDataOffset(dataOffset);
}
}
@Override @Override
protected void onAttachedToWindow() protected void onAttachedToWindow()
{ {
@@ -123,6 +150,29 @@ public class HabitCardListView extends RecyclerView
super.onDetachedFromWindow(); super.onDetachedFromWindow();
} }
@Override
protected void onRestoreInstanceState(Parcelable state)
{
if(!(state instanceof BundleSavedState))
{
super.onRestoreInstanceState(state);
return;
}
BundleSavedState bss = (BundleSavedState) state;
dataOffset = bss.bundle.getInt("dataOffset");
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
protected Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putInt("dataOffset", dataOffset);
return new BundleSavedState(superState, bundle);
}
protected void setupCardViewController(@NonNull HabitCardViewHolder holder) protected void setupCardViewController(@NonNull HabitCardViewHolder holder)
{ {
HabitCardView cardView = (HabitCardView) holder.itemView; HabitCardView cardView = (HabitCardView) holder.itemView;
@@ -168,7 +218,7 @@ public class HabitCardListView extends RecyclerView
{ {
int position = holder.getAdapterPosition(); int position = holder.getAdapterPosition();
if (controller != null) controller.onItemLongClick(position); if (controller != null) controller.onItemLongClick(position);
touchHelper.startDrag(holder); if (adapter.isSortable()) touchHelper.startDrag(holder);
} }
@Override @Override

View File

@@ -27,7 +27,7 @@ import android.support.annotation.*;
import android.util.*; import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
@@ -72,6 +72,8 @@ public class HabitCardView extends FrameLayout
@Nullable @Nullable
private Habit habit; private Habit habit;
private int dataOffset;
public HabitCardView(Context context) public HabitCardView(Context context)
{ {
super(context); super(context);
@@ -90,6 +92,11 @@ public class HabitCardView extends FrameLayout
new Handler(Looper.getMainLooper()).post(() -> refresh()); new Handler(Looper.getMainLooper()).post(() -> refresh());
} }
public void setCheckmarkCount(int checkmarkCount)
{
checkmarkPanel.setButtonCount(checkmarkCount);
}
public void setCheckmarkValues(int checkmarks[]) public void setCheckmarkValues(int checkmarks[])
{ {
checkmarkPanel.setCheckmarkValues(checkmarks); checkmarkPanel.setCheckmarkValues(checkmarks);
@@ -103,6 +110,12 @@ public class HabitCardView extends FrameLayout
checkmarkPanel.setController(controller); checkmarkPanel.setController(controller);
} }
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
checkmarkPanel.setDataOffset(dataOffset);
}
public void setHabit(@NonNull Habit habit) public void setHabit(@NonNull Habit habit)
{ {
if (this.habit != null) detachFromHabit(); if (this.habit != null) detachFromHabit();
@@ -134,7 +147,7 @@ public class HabitCardView extends FrameLayout
{ {
long today = DateUtils.getStartOfToday(); long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay; long day = DateUtils.millisecondsInOneDay;
int offset = (int) ((today - timestamp) / day); int offset = (int) ((today - timestamp) / day) - dataOffset;
CheckmarkButtonView button = checkmarkPanel.indexToButton(offset); CheckmarkButtonView button = checkmarkPanel.indexToButton(offset);
float y = button.getHeight() / 2.0f; float y = button.getHeight() / 2.0f;
@@ -201,6 +214,7 @@ public class HabitCardView extends FrameLayout
scoreRing.setPercentage(rand.nextFloat()); scoreRing.setPercentage(rand.nextFloat());
checkmarkPanel.setColor(color); checkmarkPanel.setColor(color);
checkmarkPanel.setCheckmarkValues(values); checkmarkPanel.setCheckmarkValues(values);
checkmarkPanel.setButtonCount(5);
} }
private void refresh() private void refresh()

View File

@@ -20,30 +20,39 @@
package org.isoron.uhabits.activities.habits.list.views; package org.isoron.uhabits.activities.habits.list.views;
import android.content.*; import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.text.*;
import android.util.*; import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import java.util.*; import java.util.*;
public class HeaderView extends LinearLayout implements Preferences.Listener public class HeaderView extends ScrollableChart
implements Preferences.Listener, MidnightTimer.MidnightListener
{ {
private final Context context;
private int buttonCount; private int buttonCount;
@Nullable @Nullable
private Preferences prefs; private Preferences prefs;
@Nullable
private MidnightTimer midnightTimer;
private final TextPaint paint;
private RectF rect;
public HeaderView(Context context, AttributeSet attrs) public HeaderView(Context context, AttributeSet attrs)
{ {
super(context, attrs); super(context, attrs);
this.context = context;
if (isInEditMode()) if (isInEditMode())
{ {
@@ -56,51 +65,116 @@ public class HeaderView extends LinearLayout implements Preferences.Listener
HabitsApplication app = (HabitsApplication) appContext; HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences(); prefs = app.getComponent().getPreferences();
} }
if (context instanceof ListHabitsActivity)
{
ListHabitsActivity activity = (ListHabitsActivity) context;
midnightTimer = activity.getListHabitsComponent().getMidnightTimer();
}
Resources res = context.getResources();
setScrollerBucketSize((int) res.getDimension(R.dimen.checkmarkWidth));
StyledResources sr = new StyledResources(context);
paint = new TextPaint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setTextSize(getResources().getDimension(R.dimen.tinyTextSize));
paint.setTextAlign(Paint.Align.CENTER);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setColor(sr.getColor(R.attr.mediumContrastTextColor));
rect = new RectF();
}
@Override
public void atMidnight()
{
post(() -> invalidate());
} }
@Override @Override
public void onCheckmarkOrderChanged() public void onCheckmarkOrderChanged()
{ {
createButtons(); updateDirection();
postInvalidate();
} }
public void setButtonCount(int buttonCount) public void setButtonCount(int buttonCount)
{ {
this.buttonCount = buttonCount; this.buttonCount = buttonCount;
createButtons(); postInvalidate();
} }
@Override @Override
protected void onAttachedToWindow() protected void onAttachedToWindow()
{ {
updateDirection();
super.onAttachedToWindow(); super.onAttachedToWindow();
if (prefs != null) prefs.addListener(this); if (prefs != null) prefs.addListener(this);
if (midnightTimer != null) midnightTimer.addListener(this);
}
private void updateDirection()
{
int direction = -1;
if (shouldReverseCheckmarks()) direction *= -1;
if (InterfaceUtils.isLayoutRtl(this)) direction *= -1;
setDirection(direction);
} }
@Override @Override
protected void onDetachedFromWindow() protected void onDetachedFromWindow()
{ {
if (midnightTimer != null) midnightTimer.removeListener(this);
if (prefs != null) prefs.removeListener(this); if (prefs != null) prefs.removeListener(this);
super.onDetachedFromWindow(); super.onDetachedFromWindow();
} }
private void createButtons() @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{ {
removeAllViews(); int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) getContext()
.getResources()
.getDimension(R.dimen.checkmarkHeight);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
Resources res = getContext().getResources();
float width = res.getDimension(R.dimen.checkmarkWidth);
float height = res.getDimension(R.dimen.checkmarkHeight);
boolean reverse = shouldReverseCheckmarks();
boolean isRtl = InterfaceUtils.isLayoutRtl(this);
day.add(GregorianCalendar.DAY_OF_MONTH, -getDataOffset());
float em = paint.measureText("m");
for (int i = 0; i < buttonCount; i++) for (int i = 0; i < buttonCount; i++)
addView(
inflate(context, R.layout.list_habits_header_checkmark, null));
for (int i = 0; i < getChildCount(); i++)
{ {
int position = i; rect.set(0, 0, width, height);
if (shouldReverseCheckmarks()) position = getChildCount() - i - 1; rect.offset(canvas.getWidth(), 0);
View button = getChildAt(position); if(reverse) rect.offset(- (i + 1) * width, 0);
TextView label = (TextView) button.findViewById(R.id.tvCheck); else rect.offset((i - buttonCount) * width, 0);
label.setText(DateUtils.formatHeaderDate(day));
if (isRtl) rect.set(canvas.getWidth() - rect.right, rect.top,
canvas.getWidth() - rect.left, rect.bottom);
String text = DateUtils.formatHeaderDate(day).toUpperCase();
String[] lines = text.split("\n");
int y1 = (int)(rect.centerY() - 0.25 * em);
int y2 = (int)(rect.centerY() + 1.25 * em);
canvas.drawText(lines[0], rect.centerX(), y1, paint);
canvas.drawText(lines[1], rect.centerX(), y2, paint);
day.add(GregorianCalendar.DAY_OF_MONTH, -1); day.add(GregorianCalendar.DAY_OF_MONTH, -1);
} }
} }

View File

@@ -24,7 +24,7 @@ import android.os.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.v7.widget.*; import android.support.v7.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.show.views.*; import org.isoron.uhabits.activities.habits.show.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;

View File

@@ -24,6 +24,10 @@ import android.view.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import java.util.*;
import javax.inject.*; import javax.inject.*;
@@ -33,12 +37,38 @@ public class ShowHabitsMenu extends BaseMenu
@NonNull @NonNull
private final ShowHabitScreen screen; private final ShowHabitScreen screen;
@NonNull
private final Habit habit;
@NonNull
private final TaskRunner taskRunner;
@NonNull
private ExportCSVTaskFactory exportCSVFactory;
@Inject @Inject
public ShowHabitsMenu(@NonNull BaseActivity activity, public ShowHabitsMenu(@NonNull BaseActivity activity,
@NonNull ShowHabitScreen screen) @NonNull ShowHabitScreen screen,
@NonNull Habit habit,
@NonNull ExportCSVTaskFactory exportCSVFactory,
@NonNull TaskRunner taskRunner)
{ {
super(activity); super(activity);
this.screen = screen; this.screen = screen;
this.habit = habit;
this.taskRunner = taskRunner;
this.exportCSVFactory = exportCSVFactory;
}
public void exportHabit()
{
List<Habit> selected = new LinkedList<>();
selected.add(habit);
ExportCSVTask task = exportCSVFactory.create(selected, filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
});
taskRunner.execute(task);
} }
@Override @Override
@@ -50,6 +80,10 @@ public class ShowHabitsMenu extends BaseMenu
screen.showEditHabitDialog(); screen.showEditHabitDialog();
return true; return true;
case R.id.export:
this.exportHabit();
return true;
default: default:
return false; return false;
} }

View File

@@ -25,6 +25,7 @@ import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;

View File

@@ -25,6 +25,7 @@ import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;

View File

@@ -25,6 +25,7 @@ import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;

View File

@@ -25,6 +25,7 @@ import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;

View File

@@ -25,6 +25,7 @@ import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;

View File

@@ -25,7 +25,7 @@ import android.content.res.*;
import android.util.*; import android.util.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;

View File

@@ -61,9 +61,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT); setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT);
updateRingtoneDescription(); updateRingtoneDescription();
if (InterfaceUtils.isLocaleFullyTranslated())
removePreference("translate", "linksCategory");
} }
@Override @Override
@@ -110,14 +107,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
BackupManager.dataChanged("org.isoron.uhabits"); BackupManager.dataChanged("org.isoron.uhabits");
} }
private void removePreference(String preferenceKey, String categoryKey)
{
PreferenceCategory cat =
(PreferenceCategory) findPreference(categoryKey);
Preference pref = findPreference(preferenceKey);
cat.removePreference(pref);
}
private void setResultOnPreferenceClick(String key, final int result) private void setResultOnPreferenceClick(String key, final int result)
{ {
Preference pref = findPreference(key); Preference pref = findPreference(key);

View File

@@ -25,7 +25,7 @@ import android.support.v7.widget.*;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;

View File

@@ -39,6 +39,12 @@ public class IntentFactory
{ {
} }
public Intent helpTranslate(Context context)
{
String url = context.getString(R.string.translateURL);
return buildViewIntent(url);
}
public Intent rateApp(Context context) public Intent rateApp(Context context)
{ {
String url = context.getString(R.string.playStoreURL); String url = context.getString(R.string.playStoreURL);

View File

@@ -19,18 +19,20 @@
package org.isoron.uhabits.io; package org.isoron.uhabits.io;
import android.database.Cursor; import android.content.*;
import android.database.sqlite.SQLiteDatabase; import android.database.*;
import android.support.annotation.NonNull; import android.database.sqlite.*;
import android.support.annotation.*;
import android.util.*;
import com.activeandroid.ActiveAndroid; import com.activeandroid.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.FileUtils; import org.isoron.uhabits.utils.*;
import java.io.File; import java.io.*;
import java.io.IOException;
import javax.inject.*; import javax.inject.*;
@@ -39,10 +41,15 @@ import javax.inject.*;
*/ */
public class LoopDBImporter extends AbstractImporter public class LoopDBImporter extends AbstractImporter
{ {
@NonNull
private Context context;
@Inject @Inject
public LoopDBImporter(@NonNull HabitList habits) public LoopDBImporter(@NonNull @AppContext Context context,
@NonNull HabitList habits)
{ {
super(habits); super(habits);
this.context = context;
} }
@Override @Override
@@ -53,23 +60,37 @@ public class LoopDBImporter extends AbstractImporter
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY); SQLiteDatabase.OPEN_READONLY);
boolean canHandle = true;
Cursor c = db.rawQuery( Cursor c = db.rawQuery(
"select count(*) from SQLITE_MASTER where name=? or name=?", "select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"Checkmarks", "Repetitions"}); new String[]{ "Checkmarks", "Repetitions" });
boolean result = (c.moveToFirst() && c.getInt(0) == 2); if (!c.moveToFirst() || c.getInt(0) != 2)
{
Log.w("LoopDBImporter", "Cannot handle file: tables not found");
canHandle = false;
}
if (db.getVersion() > BuildConfig.databaseVersion)
{
Log.w("LoopDBImporter", String.format(
"Cannot handle file: incompatible version: %d > %d",
db.getVersion(), BuildConfig.databaseVersion));
canHandle = false;
}
c.close(); c.close();
db.close(); db.close();
return result; return canHandle;
} }
@Override @Override
public void importHabitsFromFile(@NonNull File file) throws IOException public void importHabitsFromFile(@NonNull File file) throws IOException
{ {
ActiveAndroid.dispose(); ActiveAndroid.dispose();
File originalDB = DatabaseUtils.getDatabaseFile(); File originalDB = DatabaseUtils.getDatabaseFile(context);
FileUtils.copy(file, originalDB); FileUtils.copy(file, originalDB);
DatabaseUtils.initializeActiveAndroid(); DatabaseUtils.initializeActiveAndroid(context);
} }
} }

View File

@@ -108,7 +108,7 @@ public abstract class CheckmarkList
* *
* @return value of today's checkmark * @return value of today's checkmark
*/ */
public final int getTodayValue() public int getTodayValue()
{ {
Checkmark today = getToday(); Checkmark today = getToday();
if (today != null) return today.getValue(); if (today != null) return today.getValue();
@@ -192,7 +192,7 @@ public abstract class CheckmarkList
Checkmark newest = getNewestComputed(); Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed(); Checkmark oldest = getOldestComputed();
if (newest == null) if (newest == null || oldest == null)
{ {
forceRecompute(from, to); forceRecompute(from, to);
} }
@@ -208,6 +208,7 @@ public abstract class CheckmarkList
* *
* @return oldest checkmark already computed * @return oldest checkmark already computed
*/ */
@Nullable
protected abstract Checkmark getOldestComputed(); protected abstract Checkmark getOldestComputed();
/** /**
@@ -285,5 +286,6 @@ public abstract class CheckmarkList
* *
* @return newest checkmark already computed * @return newest checkmark already computed
*/ */
@Nullable
protected abstract Checkmark getNewestComputed(); protected abstract Checkmark getNewestComputed();
} }

View File

@@ -48,9 +48,7 @@ public abstract class HabitList implements Iterable<Habit>
public HabitList() public HabitList()
{ {
observable = new ModelObservable(); observable = new ModelObservable();
filter = new HabitMatcherBuilder() filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
.setArchivedAllowed(true)
.build();
} }
protected HabitList(@NonNull HabitMatcher filter) protected HabitList(@NonNull HabitMatcher filter)
@@ -106,6 +104,15 @@ public abstract class HabitList implements Iterable<Habit>
return observable; return observable;
} }
public abstract Order getOrder();
/**
* Changes the order of the elements on the list.
*
* @param order the new order criterea
*/
public abstract void setOrder(@NonNull Order order);
/** /**
* Returns the index of the given habit in the list, or -1 if the list does * Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit. * not contain the habit.
@@ -149,7 +156,7 @@ public abstract class HabitList implements Iterable<Habit>
public void repair() public void repair()
{ {
for(Habit h : this) for (Habit h : this)
{ {
h.getCheckmarks().invalidateNewerThan(0); h.getCheckmarks().invalidateNewerThan(0);
h.getStreaks().invalidateNewerThan(0); h.getStreaks().invalidateNewerThan(0);
@@ -228,4 +235,12 @@ public abstract class HabitList implements Iterable<Habit>
csv.close(); csv.close();
} }
public enum Order
{
BY_NAME,
BY_COLOR,
BY_SCORE,
BY_POSITION
}
} }

View File

@@ -72,6 +72,7 @@ public class MemoryCheckmarkList extends CheckmarkList
} }
@Override @Override
@Nullable
protected Checkmark getOldestComputed() protected Checkmark getOldestComputed()
{ {
if(list.isEmpty()) return null; if(list.isEmpty()) return null;
@@ -79,6 +80,7 @@ public class MemoryCheckmarkList extends CheckmarkList
} }
@Override @Override
@Nullable
protected Checkmark getNewestComputed() protected Checkmark getNewestComputed()
{ {
if(list.isEmpty()) return null; if(list.isEmpty()) return null;

View File

@@ -25,6 +25,8 @@ import org.isoron.uhabits.models.*;
import java.util.*; import java.util.*;
import static org.isoron.uhabits.models.HabitList.Order.*;
/** /**
* In-memory implementation of {@link HabitList}. * In-memory implementation of {@link HabitList}.
*/ */
@@ -33,16 +35,23 @@ public class MemoryHabitList extends HabitList
@NonNull @NonNull
private LinkedList<Habit> list; private LinkedList<Habit> list;
private Comparator<Habit> comparator = null;
@NonNull
private Order order;
public MemoryHabitList() public MemoryHabitList()
{ {
super(); super();
list = new LinkedList<>(); list = new LinkedList<>();
order = Order.BY_POSITION;
} }
protected MemoryHabitList(@NonNull HabitMatcher matcher) protected MemoryHabitList(@NonNull HabitMatcher matcher)
{ {
super(matcher); super(matcher);
list = new LinkedList<>(); list = new LinkedList<>();
order = Order.BY_POSITION;
} }
@Override @Override
@@ -57,6 +66,7 @@ public class MemoryHabitList extends HabitList
if (id == null) habit.setId((long) list.size()); if (id == null) habit.setId((long) list.size());
list.addLast(habit); list.addLast(habit);
resort();
} }
@Override @Override
@@ -82,10 +92,17 @@ public class MemoryHabitList extends HabitList
public HabitList getFiltered(HabitMatcher matcher) public HabitList getFiltered(HabitMatcher matcher)
{ {
MemoryHabitList habits = new MemoryHabitList(matcher); MemoryHabitList habits = new MemoryHabitList(matcher);
for(Habit h : this) if (matcher.matches(h)) habits.add(h); habits.comparator = comparator;
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
return habits; return habits;
} }
@Override
public Order getOrder()
{
return order;
}
@Override @Override
public int indexOf(@NonNull Habit h) public int indexOf(@NonNull Habit h)
{ {
@@ -112,6 +129,14 @@ public class MemoryHabitList extends HabitList
list.add(toPos, from); list.add(toPos, from);
} }
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override @Override
public int size() public int size()
{ {
@@ -123,4 +148,34 @@ public class MemoryHabitList extends HabitList
{ {
// NOP // NOP
} }
private Comparator<Habit> getComparatorByOrder(Order order)
{
Comparator<Habit> nameComparator =
(h1, h2) -> h1.getName().compareTo(h2.getName());
Comparator<Habit> colorComparator = (h1, h2) -> {
Integer c1 = h1.getColor();
Integer c2 = h2.getColor();
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
else return c1.compareTo(c2);
};
Comparator<Habit> scoreComparator = (h1, h2) -> {
int s1 = h1.getScores().getTodayValue();
int s2 = h2.getScores().getTodayValue();
return Integer.compare(s2, s1);
};
if (order == BY_POSITION) return null;
if (order == BY_NAME) return nameComparator;
if (order == BY_COLOR) return colorComparator;
if (order == BY_SCORE) return scoreComparator;
throw new IllegalStateException();
}
private void resort()
{
if (comparator != null) Collections.sort(list, comparator);
}
} }

View File

@@ -0,0 +1,24 @@
/*
* 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.models.sqlite;
public class InvalidDatabaseVersionException extends RuntimeException
{
}

View File

@@ -24,7 +24,6 @@ import android.support.annotation.*;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
@@ -38,38 +37,54 @@ import java.util.*;
*/ */
public class SQLiteCheckmarkList extends CheckmarkList public class SQLiteCheckmarkList extends CheckmarkList
{ {
@Nullable @Nullable
private HabitRecord habitRecord; private HabitRecord habitRecord;
@NonNull @NonNull
private final SQLiteUtils<CheckmarkRecord> sqlite; private final SQLiteUtils<CheckmarkRecord> sqlite;
@Nullable
private Integer todayValue;
@NonNull
private final SQLiteStatement invalidateStatement;
@NonNull
private final SQLiteStatement addStatement;
@NonNull
private final SQLiteDatabase db;
private static final String ADD_QUERY =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
private static final String INVALIDATE_QUERY =
"delete from Checkmarks where habit = ? and timestamp >= ?";
public SQLiteCheckmarkList(Habit habit) public SQLiteCheckmarkList(Habit habit)
{ {
super(habit); super(habit);
sqlite = new SQLiteUtils<>(CheckmarkRecord.class); sqlite = new SQLiteUtils<>(CheckmarkRecord.class);
db = Cache.openDatabase();
addStatement = db.compileStatement(ADD_QUERY);
invalidateStatement = db.compileStatement(INVALIDATE_QUERY);
} }
@Override @Override
public void add(List<Checkmark> checkmarks) public void add(List<Checkmark> checkmarks)
{ {
check(habit.getId()); check(habit.getId());
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction(); db.beginTransaction();
try try
{ {
SQLiteStatement statement = db.compileStatement(query);
for (Checkmark c : checkmarks) for (Checkmark c : checkmarks)
{ {
statement.bindLong(1, habit.getId()); addStatement.bindLong(1, habit.getId());
statement.bindLong(2, c.getTimestamp()); addStatement.bindLong(2, c.getTimestamp());
statement.bindLong(3, c.getValue()); addStatement.bindLong(3, c.getValue());
statement.execute(); addStatement.execute();
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();
@@ -115,12 +130,10 @@ public class SQLiteCheckmarkList extends CheckmarkList
@Override @Override
public void invalidateNewerThan(long timestamp) public void invalidateNewerThan(long timestamp)
{ {
new Delete() todayValue = null;
.from(CheckmarkRecord.class) invalidateStatement.bindLong(1, habit.getId());
.where("habit = ?", habit.getId()) invalidateStatement.bindLong(2, timestamp);
.and("timestamp >= ?", timestamp) invalidateStatement.execute();
.execute();
observable.notifyListeners(); observable.notifyListeners();
} }
@@ -140,6 +153,7 @@ public class SQLiteCheckmarkList extends CheckmarkList
} }
@Override @Override
@Nullable
protected Checkmark getOldestComputed() protected Checkmark getOldestComputed()
{ {
check(habit.getId()); check(habit.getId());
@@ -179,4 +193,11 @@ public class SQLiteCheckmarkList extends CheckmarkList
for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark()); for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark());
return checkmarks; return checkmarks;
} }
@Override
public int getTodayValue()
{
if(todayValue == null) todayValue = super.getTodayValue();
return todayValue;
}
} }

View File

@@ -39,10 +39,15 @@ public class SQLiteHabitList extends HabitList
private static SQLiteHabitList instance; private static SQLiteHabitList instance;
@NonNull
private final SQLiteUtils<HabitRecord> sqlite; private final SQLiteUtils<HabitRecord> sqlite;
@NonNull
private final ModelFactory modelFactory; private final ModelFactory modelFactory;
@NonNull
private Order order;
public SQLiteHabitList(@NonNull ModelFactory modelFactory) public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{ {
super(); super();
@@ -50,16 +55,19 @@ public class SQLiteHabitList extends HabitList
if (cache == null) cache = new HashMap<>(); if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class); sqlite = new SQLiteUtils<>(HabitRecord.class);
order = Order.BY_POSITION;
} }
protected SQLiteHabitList(@NonNull ModelFactory modelFactory, protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
@NonNull HabitMatcher filter) @NonNull HabitMatcher filter,
@NonNull Order order)
{ {
super(filter); super(filter);
this.modelFactory = modelFactory; this.modelFactory = modelFactory;
if (cache == null) cache = new HashMap<>(); if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class); sqlite = new SQLiteUtils<>(HabitRecord.class);
this.order = order;
} }
public static SQLiteHabitList getInstance( public static SQLiteHabitList getInstance(
@@ -118,7 +126,20 @@ public class SQLiteHabitList extends HabitList
@Override @Override
public HabitList getFiltered(HabitMatcher filter) public HabitList getFiltered(HabitMatcher filter)
{ {
return new SQLiteHabitList(modelFactory, filter); return new SQLiteHabitList(modelFactory, filter, order);
}
@Override
@NonNull
public Order getOrder()
{
return order;
}
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
} }
@Override @Override
@@ -214,6 +235,13 @@ public class SQLiteHabitList extends HabitList
getObservable().notifyListeners(); getObservable().notifyListeners();
} }
@Override
public void repair()
{
super.repair();
rebuildOrder();
}
@Override @Override
public int size() public int size()
{ {
@@ -233,7 +261,7 @@ public class SQLiteHabitList extends HabitList
} }
} }
protected List<Habit> toList() protected synchronized List<Habit> toList()
{ {
String query = buildSelectQuery(); String query = buildSelectQuery();
List<HabitRecord> recordList = sqlite.query(query, null); List<HabitRecord> recordList = sqlite.query(query, null);
@@ -249,12 +277,38 @@ public class SQLiteHabitList extends HabitList
habits.add(habit); habits.add(habit);
} }
if(order == Order.BY_SCORE)
{
Collections.sort(habits, (lhs, rhs) -> {
int s1 = lhs.getScores().getTodayValue();
int s2 = rhs.getScores().getTodayValue();
return Integer.compare(s2, s1);
});
}
return habits; return habits;
} }
private void appendOrderBy(StringBuilder query) private void appendOrderBy(StringBuilder query)
{ {
query.append("order by position "); switch (order)
{
case BY_POSITION:
query.append("order by position ");
break;
case BY_NAME:
case BY_SCORE:
query.append("order by name ");
break;
case BY_COLOR:
query.append("order by color, name ");
break;
default:
throw new IllegalStateException();
}
} }
private void appendSelect(StringBuilder query) private void appendSelect(StringBuilder query)
@@ -282,11 +336,4 @@ public class SQLiteHabitList extends HabitList
appendOrderBy(query); appendOrderBy(query);
return query.toString(); return query.toString();
} }
@Override
public void repair()
{
super.repair();
rebuildOrder();
}
} }

View File

@@ -19,12 +19,12 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.DatabaseUtils; import android.database.*;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.Cache; import com.activeandroid.*;
import com.activeandroid.query.*; import com.activeandroid.query.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
@@ -43,10 +43,16 @@ public class SQLiteRepetitionList extends RepetitionList
@Nullable @Nullable
private HabitRecord habitRecord; private HabitRecord habitRecord;
private SQLiteStatement addStatement;
public SQLiteRepetitionList(@NonNull Habit habit) public SQLiteRepetitionList(@NonNull Habit habit)
{ {
super(habit); super(habit);
sqlite = new SQLiteUtils<>(RepetitionRecord.class); sqlite = new SQLiteUtils<>(RepetitionRecord.class);
SQLiteDatabase db = Cache.openDatabase();
String addQuery = "insert into repetitions(habit, timestamp) values (?,?)";
addStatement = db.compileStatement(addQuery);
} }
/** /**
@@ -61,11 +67,9 @@ public class SQLiteRepetitionList extends RepetitionList
public void add(Repetition rep) public void add(Repetition rep)
{ {
check(habit.getId()); check(habit.getId());
addStatement.bindLong(1, habit.getId());
RepetitionRecord record = new RepetitionRecord(); addStatement.bindLong(2, rep.getTimestamp());
record.copyFrom(rep); addStatement.execute();
record.habit = habitRecord;
record.save();
observable.notifyListeners(); observable.notifyListeners();
} }

View File

@@ -24,7 +24,6 @@ import android.support.annotation.*;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
@@ -37,12 +36,30 @@ import java.util.*;
*/ */
public class SQLiteScoreList extends ScoreList public class SQLiteScoreList extends ScoreList
{ {
@Nullable @Nullable
private HabitRecord habitRecord; private HabitRecord habitRecord;
@NonNull @NonNull
private final SQLiteUtils<ScoreRecord> sqlite; private final SQLiteUtils<ScoreRecord> sqlite;
@Nullable
private Integer todayValue;
@NonNull
private final SQLiteStatement invalidateStatement;
@NonNull
private final SQLiteStatement addStatement;
public static final String ADD_QUERY =
"insert into Score(habit, timestamp, score) values (?,?,?)";
public static final String INVALIDATE_QUERY =
"delete from Score where habit = ? " + "and timestamp >= ?";
private final SQLiteDatabase db;
/** /**
* Constructs a new ScoreList associated with the given habit. * Constructs a new ScoreList associated with the given habit.
* *
@@ -52,28 +69,25 @@ public class SQLiteScoreList extends ScoreList
{ {
super(habit); super(habit);
sqlite = new SQLiteUtils<>(ScoreRecord.class); sqlite = new SQLiteUtils<>(ScoreRecord.class);
db = Cache.openDatabase();
addStatement = db.compileStatement(ADD_QUERY);
invalidateStatement = db.compileStatement(INVALIDATE_QUERY);
} }
@Override @Override
public void add(List<Score> scores) public void add(List<Score> scores)
{ {
check(habit.getId()); check(habit.getId());
String query =
"insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction(); db.beginTransaction();
try try
{ {
SQLiteStatement statement = db.compileStatement(query);
for (Score s : scores) for (Score s : scores)
{ {
statement.bindLong(1, habit.getId()); addStatement.bindLong(1, habit.getId());
statement.bindLong(2, s.getTimestamp()); addStatement.bindLong(2, s.getTimestamp());
statement.bindLong(3, s.getValue()); addStatement.bindLong(3, s.getValue());
statement.execute(); addStatement.execute();
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();
@@ -126,12 +140,10 @@ public class SQLiteScoreList extends ScoreList
@Override @Override
public void invalidateNewerThan(long timestamp) public void invalidateNewerThan(long timestamp)
{ {
new Delete() todayValue = null;
.from(ScoreRecord.class) invalidateStatement.bindLong(1, habit.getId());
.where("habit = ?", habit.getId()) invalidateStatement.bindLong(2, timestamp);
.and("timestamp >= ?", timestamp) invalidateStatement.execute();
.execute();
getObservable().notifyListeners(); getObservable().notifyListeners();
} }
@@ -204,4 +216,11 @@ public class SQLiteScoreList extends ScoreList
for (ScoreRecord r : records) scores.add(r.toScore()); for (ScoreRecord r : records) scores.add(r.toScore());
return scores; return scores;
} }
@Override
public int getTodayValue()
{
if (todayValue == null) todayValue = super.getTodayValue();
return todayValue;
}
} }

View File

@@ -19,10 +19,11 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.sqlite.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.query.*; import com.activeandroid.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
@@ -41,10 +42,17 @@ public class SQLiteStreakList extends StreakList
@NonNull @NonNull
private final SQLiteUtils<StreakRecord> sqlite; private final SQLiteUtils<StreakRecord> sqlite;
private final SQLiteStatement invalidateStatement;
public SQLiteStreakList(Habit habit) public SQLiteStreakList(Habit habit)
{ {
super(habit); super(habit);
sqlite = new SQLiteUtils<>(StreakRecord.class); sqlite = new SQLiteUtils<>(StreakRecord.class);
SQLiteDatabase db = Cache.openDatabase();
String invalidateQuery = "delete from Streak where habit = ? " +
"and end >= ?";
invalidateStatement = db.compileStatement(invalidateQuery);
} }
@Override @Override
@@ -73,12 +81,9 @@ public class SQLiteStreakList extends StreakList
@Override @Override
public void invalidateNewerThan(long timestamp) public void invalidateNewerThan(long timestamp)
{ {
new Delete() invalidateStatement.bindLong(1, habit.getId());
.from(StreakRecord.class) invalidateStatement.bindLong(2, timestamp - DateUtils.millisecondsInOneDay);
.where("habit = ?", habit.getId()) invalidateStatement.execute();
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
observable.notifyListeners(); observable.notifyListeners();
} }

View File

@@ -22,6 +22,8 @@ package org.isoron.uhabits.models.sqlite.records;
import android.annotation.*; import android.annotation.*;
import android.database.*; import android.database.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.*; import com.activeandroid.*;
import com.activeandroid.annotation.*; import com.activeandroid.annotation.*;

View File

@@ -24,6 +24,7 @@ import android.preference.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
import java.util.*; import java.util.*;
@@ -61,6 +62,21 @@ public class Preferences
return prefs.getInt("pref_default_habit_palette_color", fallbackColor); return prefs.getInt("pref_default_habit_palette_color", fallbackColor);
} }
public HabitList.Order getDefaultOrder()
{
String name = prefs.getString("pref_default_order", "BY_POSITION");
try
{
return HabitList.Order.valueOf(name);
}
catch (IllegalArgumentException e)
{
setDefaultOrder(HabitList.Order.BY_POSITION);
return HabitList.Order.BY_POSITION;
}
}
public int getDefaultScoreSpinnerPosition() public int getDefaultScoreSpinnerPosition()
{ {
int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1); int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1);
@@ -69,6 +85,11 @@ public class Preferences
return defaultScoreInterval; return defaultScoreInterval;
} }
public void setDefaultOrder(HabitList.Order order)
{
prefs.edit().putString("pref_default_order", order.name()).apply();
}
public void setDefaultScoreSpinnerPosition(int position) public void setDefaultScoreSpinnerPosition(int position)
{ {
prefs.edit().putInt("pref_score_view_interval", position).apply(); prefs.edit().putInt("pref_score_view_interval", position).apply();

View File

@@ -23,6 +23,7 @@ import android.content.*;
import android.preference.*; import android.preference.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import javax.inject.*; import javax.inject.*;
@@ -48,7 +49,7 @@ public class WidgetPreferences
public long getHabitIdFromWidgetId(int widgetId) public long getHabitIdFromWidgetId(int widgetId)
{ {
Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1); Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1);
if (habitId < 0) throw new RuntimeException("widget not found"); if (habitId < 0) throw new HabitNotFoundException();
return habitId; return habitId;
} }

View File

@@ -40,6 +40,9 @@ public class PebbleReceiver extends PebbleDataReceiver
public static final UUID WATCHAPP_UUID = public static final UUID WATCHAPP_UUID =
UUID.fromString("82629d99-8ea6-4631-a022-9ca77a12a058"); UUID.fromString("82629d99-8ea6-4631-a022-9ca77a12a058");
@NonNull
private Context context;
private HabitList allHabits; private HabitList allHabits;
private CommandRunner commandRunner; private CommandRunner commandRunner;
@@ -61,6 +64,8 @@ public class PebbleReceiver extends PebbleDataReceiver
if (context == null) throw new RuntimeException("context is null"); if (context == null) throw new RuntimeException("context is null");
if (data == null) throw new RuntimeException("data is null"); if (data == null) throw new RuntimeException("data is null");
this.context = context;
HabitsApplication app = HabitsApplication app =
(HabitsApplication) context.getApplicationContext(); (HabitsApplication) context.getApplicationContext();
@@ -136,7 +141,7 @@ public class PebbleReceiver extends PebbleDataReceiver
private void sendDict(@NonNull PebbleDictionary dict) private void sendDict(@NonNull PebbleDictionary dict)
{ {
PebbleKit.sendDataToPebble(HabitsApplication.getContext(), PebbleKit.sendDataToPebble(context,
PebbleReceiver.WATCHAPP_UUID, dict); PebbleReceiver.WATCHAPP_UUID, dict);
} }

View File

@@ -19,10 +19,13 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.content.Context;
import android.support.annotation.*; import android.support.annotation.*;
import com.google.auto.factory.*; import com.google.auto.factory.*;
import org.isoron.uhabits.AppContext;
import org.isoron.uhabits.activities.ActivityContext;
import org.isoron.uhabits.io.*; import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
@@ -35,6 +38,9 @@ public class ExportCSVTask implements Task
{ {
private String archiveFilename; private String archiveFilename;
@NonNull
private final Context context;
@NonNull @NonNull
private final List<Habit> selectedHabits; private final List<Habit> selectedHabits;
@@ -44,10 +50,12 @@ public class ExportCSVTask implements Task
@NonNull @NonNull
private final HabitList habitList; private final HabitList habitList;
public ExportCSVTask(@Provided @NonNull HabitList habitList, public ExportCSVTask(@Provided @AppContext @NonNull Context context,
@Provided @NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits, @NonNull List<Habit> selectedHabits,
@NonNull Listener listener) @NonNull Listener listener)
{ {
this.context = context;
this.listener = listener; this.listener = listener;
this.habitList = habitList; this.habitList = habitList;
this.selectedHabits = selectedHabits; this.selectedHabits = selectedHabits;
@@ -58,7 +66,7 @@ public class ExportCSVTask implements Task
{ {
try try
{ {
File dir = FileUtils.getFilesDir("CSV"); File dir = FileUtils.getFilesDir(context, "CSV");
if (dir == null) return; if (dir == null) return;
HabitsCSVExporter exporter; HabitsCSVExporter exporter;

View File

@@ -19,22 +19,33 @@
package org.isoron.uhabits.tasks; package org.isoron.uhabits.tasks;
import android.content.Context;
import android.support.annotation.*; import android.support.annotation.*;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import org.isoron.uhabits.AppContext;
import org.isoron.uhabits.activities.ActivityContext;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import java.io.*; import java.io.*;
@AutoFactory(allowSubclasses = true)
public class ExportDBTask implements Task public class ExportDBTask implements Task
{ {
private String filename; private String filename;
@NonNull
private Context context;
@NonNull @NonNull
private final Listener listener; private final Listener listener;
public ExportDBTask(@NonNull Listener listener) public ExportDBTask(@Provided @AppContext @NonNull Context context, @NonNull Listener listener)
{ {
this.listener = listener; this.listener = listener;
this.context = context;
} }
@Override @Override
@@ -44,10 +55,10 @@ public class ExportDBTask implements Task
try try
{ {
File dir = FileUtils.getFilesDir("Backups"); File dir = FileUtils.getFilesDir(context, "Backups");
if (dir == null) return; if (dir == null) return;
filename = DatabaseUtils.saveDatabaseCopy(dir); filename = DatabaseUtils.saveDatabaseCopy(context, dir);
} }
catch (IOException e) catch (IOException e)
{ {

View File

@@ -74,4 +74,14 @@ public class AttributeSetUtils
if (number != null) return Float.parseFloat(number); if (number != null) return Float.parseFloat(number);
else return defaultValue; else return defaultValue;
} }
public static int getIntAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
int defaultValue)
{
String number = getAttribute(context, attrs, name, null);
if (number != null) return Integer.parseInt(number);
else return defaultValue;
}
} }

View File

@@ -25,6 +25,7 @@ import android.support.annotation.*;
import com.activeandroid.*; import com.activeandroid.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*; import org.isoron.uhabits.models.sqlite.records.*;
import java.io.*; import java.io.*;
@@ -47,9 +48,8 @@ public abstract class DatabaseUtils
} }
@NonNull @NonNull
public static File getDatabaseFile() public static File getDatabaseFile(Context context)
{ {
Context context = HabitsApplication.getContext();
String databaseFilename = getDatabaseFilename(); String databaseFilename = getDatabaseFilename();
String root = context.getFilesDir().getPath(); String root = context.getFilesDir().getPath();
@@ -68,9 +68,8 @@ public abstract class DatabaseUtils
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static void initializeActiveAndroid() public static void initializeActiveAndroid(Context context)
{ {
Context context = HabitsApplication.getContext();
Configuration dbConfig = new Configuration.Builder(context) Configuration dbConfig = new Configuration.Builder(context)
.setDatabaseName(getDatabaseFilename()) .setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion) .setDatabaseVersion(BuildConfig.databaseVersion)
@@ -78,18 +77,27 @@ public abstract class DatabaseUtils
RepetitionRecord.class, ScoreRecord.class, StreakRecord.class) RepetitionRecord.class, ScoreRecord.class, StreakRecord.class)
.create(); .create();
ActiveAndroid.initialize(dbConfig); try
{
ActiveAndroid.initialize(dbConfig);
}
catch (RuntimeException e)
{
if(e.getMessage().contains("downgrade"))
throw new InvalidDatabaseVersionException();
else throw e;
}
} }
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(File dir) throws IOException public static String saveDatabaseCopy(Context context, File dir) throws IOException
{ {
SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat(); SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime()); String date = dateFormat.format(DateUtils.getLocalTime());
String format = "%s/Loop Habits Backup %s.db"; String format = "%s/Loop Habits Backup %s.db";
String filename = String.format(format, dir.getAbsolutePath(), date); String filename = String.format(format, dir.getAbsolutePath(), date);
File db = getDatabaseFile(); File db = getDatabaseFile(context);
File dbCopy = new File(filename); File dbCopy = new File(filename);
FileUtils.copy(db, dbCopy); FileUtils.copy(db, dbCopy);

View File

@@ -44,7 +44,7 @@ public class DateFormats
{ {
Locale locale = Locale.getDefault(); Locale locale = Locale.getDefault();
if (SDK_INT >= JELLY_BEAN) if (SDK_INT >= JELLY_BEAN_MR2)
skeleton = getBestDateTimePattern(locale, skeleton); skeleton = getBestDateTimePattern(locale, skeleton);
return fromSkeleton(skeleton, locale); return fromSkeleton(skeleton, locale);

View File

@@ -187,6 +187,11 @@ public abstract class DateUtils
return getStartOfDay(DateUtils.getLocalTime()); return getStartOfDay(DateUtils.getLocalTime());
} }
public static long millisecondsUntilTomorrow()
{
return getStartOfToday() + millisecondsInOneDay - getLocalTime();
}
public static GregorianCalendar getStartOfTodayCalendar() public static GregorianCalendar getStartOfTodayCalendar()
{ {
return getCalendar(getStartOfToday()); return getCalendar(getStartOfToday());

View File

@@ -87,9 +87,8 @@ public abstract class FileUtils
} }
@Nullable @Nullable
public static File getFilesDir(@Nullable String relativePath) public static File getFilesDir(@NonNull Context context, @Nullable String relativePath)
{ {
Context context = HabitsApplication.getContext();
File externalFilesDirs[] = File externalFilesDirs[] =
ContextCompat.getExternalFilesDirs(context, null); ContextCompat.getExternalFilesDirs(context, null);

View File

@@ -22,19 +22,12 @@ package org.isoron.uhabits.utils;
import android.content.*; import android.content.*;
import android.content.res.*; import android.content.res.*;
import android.graphics.*; import android.graphics.*;
import android.support.v4.view.*;
import android.util.*; import android.util.*;
import android.view.*;
import java.util.*;
public abstract class InterfaceUtils public abstract class InterfaceUtils
{ {
// TODO: Move this to another place, or detect automatically
private static String fullyTranslatedLanguages[] = {
"ca", "zh", "en", "de", "in", "it", "ko", "pl", "pt", "es", "tk", "uk",
"ja", "fr", "hr", "sl"
};
private static Typeface fontAwesome; private static Typeface fontAwesome;
public static Typeface getFontAwesome(Context context) public static Typeface getFontAwesome(Context context)
@@ -59,14 +52,9 @@ public abstract class InterfaceUtils
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
} }
public static boolean isLocaleFullyTranslated() public static boolean isLayoutRtl(View view)
{ {
final String currentLanguage = Locale.getDefault().getLanguage(); return ViewCompat.getLayoutDirection(view) ==
ViewCompat.LAYOUT_DIRECTION_RTL;
for(String lang : fullyTranslatedLanguages)
if(currentLanguage.equals(lang)) return true;
return false;
} }
} }

View File

@@ -0,0 +1,77 @@
/*
* 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.utils;
import org.isoron.uhabits.activities.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
/**
* A class that emits events when a new day starts.
*/
@ActivityScope
public class MidnightTimer
{
private final List<MidnightListener> listeners;
private ScheduledExecutorService executor;
@Inject
public MidnightTimer()
{
this.listeners = new LinkedList<>();
}
public synchronized void addListener(MidnightListener listener)
{
this.listeners.add(listener);
}
public synchronized void onPause()
{
executor.shutdownNow();
}
public synchronized void onResume()
{
executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> notifyListeners(),
DateUtils.millisecondsUntilTomorrow() + 1000,
DateUtils.millisecondsInOneDay, TimeUnit.MILLISECONDS);
}
public synchronized void removeListener(MidnightListener listener)
{
this.listeners.remove(listener);
}
private synchronized void notifyListeners()
{
for (MidnightListener l : listeners) l.atMidnight();
}
public interface MidnightListener
{
void atMidnight();
}
}

View File

@@ -25,6 +25,8 @@ import android.os.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.widget.*; import android.widget.*;
import com.activeandroid.util.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.preferences.*;
@@ -76,8 +78,15 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
for (int id : ids) for (int id : ids)
{ {
BaseWidget widget = getWidgetFromId(context, id); try
widget.delete(); {
BaseWidget widget = getWidgetFromId(context, id);
widget.delete();
}
catch (HabitNotFoundException e)
{
Log.e("BaseWidgetProvider", e);
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

View File

@@ -31,6 +31,7 @@
style="@style/Toolbar"/> style="@style/Toolbar"/>
<ScrollView <ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/toolbar"> android:layout_below="@id/toolbar">
@@ -86,6 +87,11 @@
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/pref_send_feedback"/> android:text="@string/pref_send_feedback"/>
<TextView
android:id="@+id/tvTranslate"
style="@style/About.Item.Clickable"
android:text="@string/help_translate"/>
<TextView <TextView
android:id="@+id/tvSource" android:id="@+id/tvSource"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
@@ -117,6 +123,9 @@
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="Nikhil (regularcoder)"/> android:text="Nikhil (regularcoder)"/>
<TextView
style="@style/About.Item"
android:text="JanetQC"/>
</LinearLayout> </LinearLayout>
@@ -129,17 +138,33 @@
android:text="@string/translators" android:text="@string/translators"
android:textColor="?aboutScreenColor"/> android:textColor="?aboutScreenColor"/>
<TextView
style="@style/About.Item"
android:text="Mihail Stefanov (Bǎlgarski)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="Angga Rifandi (Bahasa Indonesia)"/> android:text="Angga Rifandi (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="David Nos (Català)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="Tomáš Borovec (Čeština)"/> android:text="Tomáš Borovec (Čeština)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="David Nos (Català)"/> android:text="Rancher (Cрпски)"/>
<TextView
style="@style/About.Item"
android:text="Yussuf (Dansk)"/>
<TextView
style="@style/About.Item"
android:text="Sølv Ræven (Dansk)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
@@ -157,6 +182,22 @@
style="@style/About.Item" style="@style/About.Item"
android:text="Ander Raso Vazquez (Español)"/> android:text="Ander Raso Vazquez (Español)"/>
<TextView
style="@style/About.Item"
android:text="Beriain (Euskara)"/>
<TextView
style="@style/About.Item"
android:text="Andreas Michelakis (Ελληνικά)"/>
<TextView
style="@style/About.Item"
android:text="Eman (Fārsi)"/>
<TextView
style="@style/About.Item"
android:text="Saeed Esmaili (Fārsi)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="François Mahé (Français)"/> android:text="François Mahé (Français)"/>
@@ -205,6 +246,10 @@
style="@style/About.Item" style="@style/About.Item"
android:text="Dmitriy Bogdanov (Русский)"/> android:text="Dmitriy Bogdanov (Русский)"/>
<TextView
style="@style/About.Item"
android:text="Andrei Pleș (Română)"/>
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="Dušan Strgar (Slovenščina)"/> android:text="Dušan Strgar (Slovenščina)"/>
@@ -275,7 +320,12 @@
<TextView <TextView
style="@style/About.Item" style="@style/About.Item"
android:text="Andreas Michelakis (Ελληνικά)"/> android:text="Niraj Yadav (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Yoav Argov (עברית‎)"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -79,7 +79,7 @@
<EditText <EditText
android:id="@+id/tvFreqNum" android:id="@+id/tvFreqNum"
style="@style/dialogFormInputSmallNumber"/> style="@style/dialogFormInputLargeNumber"/>
<TextView <TextView
android:id="@+id/textView3" android:id="@+id/textView3"
@@ -89,7 +89,7 @@
<EditText <EditText
android:id="@+id/tvFreqDen" android:id="@+id/tvFreqDen"
style="@style/dialogFormInputSmallNumber"/> style="@style/dialogFormInputLargeNumber"/>
<TextView <TextView
android:id="@+id/textView5" android:id="@+id/textView5"

View File

@@ -21,6 +21,7 @@
<ScrollView <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/toolbar" android:layout_below="@id/toolbar"

View File

@@ -45,6 +45,26 @@
android:checkable="true" android:checkable="true"
android:enabled="true" android:enabled="true"
android:title="@string/hide_completed"/> android:title="@string/hide_completed"/>
<item android:title="@string/sort">
<menu>
<item
android:id="@+id/actionSortManual"
android:title="@string/manually"/>
<item
android:id="@+id/actionSortName"
android:title="@string/by_name"/>
<item
android:id="@+id/actionSortColor"
android:title="@string/by_color"/>
<item
android:id="@+id/actionSortScore"
android:title="@string/by_score"/>
</menu>
</item>
</menu> </menu>
</item> </item>
@@ -73,5 +93,4 @@
android:orderInCategory="100" android:orderInCategory="100"
android:title="@string/about" android:title="@string/about"
app:showAsAction="never"/> app:showAsAction="never"/>
</menu> </menu>

View File

@@ -21,6 +21,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/export"
android:title="@string/export"
app:showAsAction="never"/>
<item <item
android:id="@+id/action_edit_habit" android:id="@+id/action_edit_habit"
android:icon="?iconEdit" android:icon="?iconEdit"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<!--
~ 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/>.
-->
<resources>
<!-- App introduction -->
<!-- Middle part of the sentence '1 time in xx days' -->
</resources>

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