Compare commits

..

92 Commits

Author SHA1 Message Date
540a618ba8 Merge branch 'hotfix/1.7.6' 2017-07-18 22:45:50 -04:00
bf24cc608c Update changelog 2017-07-18 22:45:36 -04:00
a73459784e Fix NPE in HabitCardView.triggerRipple 2017-07-18 22:16:11 -04:00
de3b97dfdf Fix IndexOutOfBoundsException in HabitCardListAdapter 2017-07-18 22:13:10 -04:00
91996924d9 Never store reference to SQLiteDatabase
SQLiteDatabase closes automatically at random times. Storing a reference
to an open SQLiteDatabase eventually leads to a crash.
See: https://stackoverflow.com/questions/1483629/
2017-07-18 22:07:21 -04:00
9fe446b424 Fix crash when tapping HistoryChart 2017-07-18 21:55:15 -04:00
3857eaf5e9 Update changelog 2017-07-18 21:47:25 -04:00
e29fb58922 Fix failing tests 2017-07-18 21:47:19 -04:00
404fc869b0 Repair inconsistent data instead of throwing exception 2017-07-18 21:28:48 -04:00
001dd5a7c1 Update translations 2017-07-18 18:39:20 -04:00
7930cc8f31 Bump version to 1.7.6 2017-07-18 17:52:21 -04:00
526830ba61 Merge branch 'hotfix/1.7.5' 2017-06-10 18:56:22 -04:00
fe1513bb64 Fix race conditions on SQLiteScoreList 2017-06-10 16:48:53 -04:00
e06ace9ea8 Fix ArrayIndexOutOfBoundsException on FrequencyChart 2017-06-10 15:38:21 -04:00
d727dabb2b Fix snooze button on notifications 2017-06-10 15:30:51 -04:00
d17e8fcbfb Bump version to 1.7.5 2017-06-10 15:28:12 -04:00
be3d7145ab Merge branch 'hotfix/1.7.4' 2017-06-04 17:36:17 -04:00
cf66587644 ScoreList: make getValue synchronized 2017-06-04 17:29:41 -04:00
0dc9ec2e5f Expire caches on SQLite lists 2017-06-04 17:24:18 -04:00
0a375ded96 Silently ignore missing habits 2017-06-04 17:07:07 -04:00
fa5d6f8fee Bump version 2017-06-04 17:01:40 -04:00
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
162 changed files with 7886 additions and 5213 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,5 +1,43 @@
# Changelog
### 1.7.6 (July 18, 2017)
* Fix bug that caused widgets not to render sometimes
* Fix other minor bugs
* Update translations
### 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)
* Add option to make notifications sticky

View File

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

View File

@@ -24,6 +24,7 @@ import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.test.*;
import android.util.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
@@ -31,6 +32,7 @@ import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
@@ -63,6 +65,10 @@ public class BaseAndroidTest
protected AndroidTestComponent component;
protected ModelFactory modelFactory;
private boolean isDone = false;
@Before
public void setUp()
{
@@ -89,7 +95,7 @@ public class BaseAndroidTest
taskRunner = component.getTaskRunner();
logger = component.getHabitsLogger();
ModelFactory modelFactory = component.getModelFactory();
modelFactory = component.getModelFactory();
fixtures = new HabitFixtures(modelFactory, habitList);
latch = new CountDownLatch(1);
@@ -113,6 +119,25 @@ public class BaseAndroidTest
assertTrue(latch.await(60, TimeUnit.SECONDS));
}
protected void runConcurrently(Runnable... runnableList) throws Exception
{
isDone = false;
ExecutorService executor = Executors.newFixedThreadPool(100);
List<Future> futures = new LinkedList<>();
for (Runnable r : runnableList)
futures.add(executor.submit(() ->
{
while (!isDone) r.run();
return null;
}));
Thread.sleep(3000);
isDone = true;
executor.shutdown();
for(Future f : futures) f.get();
while (!executor.isTerminated()) Thread.sleep(50);
}
protected void setTheme(@StyleRes int themeId)
{
targetContext.setTheme(themeId);
@@ -130,4 +155,18 @@ public class BaseAndroidTest
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
{
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(
"Could not find suitable dir for screenshots");

View File

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

View File

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

View File

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

View File

@@ -171,13 +171,13 @@ public class MainTest
clickMenuItem(R.string.archive);
assertHabitsDontExist(names);
clickMenuItem(R.string.show_archived);
clickMenuItem(R.string.hide_archived);
assertHabitsExist(names);
selectHabits(names);
clickMenuItem(R.string.unarchive);
clickMenuItem(R.string.show_archived);
clickMenuItem(R.string.hide_archived);
assertHabitsExist(names);
deleteHabits(names);

View File

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

@@ -115,6 +115,41 @@ public class SQLiteCheckmarkListTest extends BaseAndroidTest
assertThat(records.get(0).timestamp, equalTo(today - 21 * day));
}
@Test
public void testFixRecords() throws Exception
{
long day = DateUtils.millisecondsInOneDay;
long from = DateUtils.getStartOfToday();
long to = from + 5 * day;
List<CheckmarkRecord> original, actual, expected;
HabitRecord habit = new HabitRecord();
original = new ArrayList<>();
original.add(new CheckmarkRecord(habit, from + 8*day, 2));
original.add(new CheckmarkRecord(habit, from + 5*day, 0));
original.add(new CheckmarkRecord(habit, from + 4*day, 0));
original.add(new CheckmarkRecord(habit, from + 4*day, 2));
original.add(new CheckmarkRecord(habit, from + 3*day, 2));
original.add(new CheckmarkRecord(habit, from + 2*day, 1));
original.add(new CheckmarkRecord(habit, from + 2*day + 100, 1));
original.add(new CheckmarkRecord(habit, from, 0));
original.add(new CheckmarkRecord(habit, from, 2));
original.add(new CheckmarkRecord(habit, from - day, 2));
actual = SQLiteCheckmarkList.fixRecords(original, habit, from, to);
expected = new ArrayList<>();
expected.add(new CheckmarkRecord(habit, from + 5*day, 0));
expected.add(new CheckmarkRecord(habit, from + 4*day, 2));
expected.add(new CheckmarkRecord(habit, from + 3*day, 2));
expected.add(new CheckmarkRecord(habit, from + 2*day, 1));
expected.add(new CheckmarkRecord(habit, from + day, 0));
expected.add(new CheckmarkRecord(habit, from, 2));
assertThat(actual, equalTo(expected));
}
private List<CheckmarkRecord> getAllRecords()
{
return new Select()

View File

@@ -125,17 +125,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
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
public void testGetById()
{
@@ -178,45 +167,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
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)
{
return new Select()

View File

@@ -61,29 +61,6 @@ public class SQLiteScoreListTest extends BaseAndroidTest
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testGetAll()
{
List<Score> list = scores.toList();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
@Test
public void testAdd()
{
@@ -101,6 +78,15 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(records.get(0).timestamp, equalTo(today));
}
@Test
public void testGetAll()
{
List<Score> list = scores.toList();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval()
{
@@ -115,6 +101,16 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval_concurrent() throws Exception
{
Runnable block1 = () -> scores.invalidateNewerThan(0);
Runnable block2 =
() -> assertThat(scores.getByInterval(today, today).size(),
equalTo(1));
runConcurrently(block1, block2);
}
@Test
public void testGetByInterval_withLongInterval()
{
@@ -125,6 +121,30 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(list.size(), equalTo(201));
}
@Test
public void testGetTodayValue_concurrent() throws Exception
{
Runnable block1 = () -> scores.invalidateNewerThan(0);
Runnable block2 =
() -> assertThat(scores.getTodayValue(), equalTo(18407827));
runConcurrently(block1, block2);
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
private List<ScoreRecord> getAllRecords()
{
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);
taskRunner.execute(
new ExportCSVTask(habitList, selected, archiveFilename -> {
new ExportCSVTask(targetContext,habitList, selected, archiveFilename -> {
assertThat(archiveFilename, is(not(nullValue())));
File f = new File(archiveFilename);
assertTrue(f.exists());

View File

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

View File

@@ -21,8 +21,8 @@
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="23"
android:versionName="1.6.0">
android:versionCode="33"
android:versionName="1.7.6">
<uses-permission android:name="android.permission.VIBRATE"/>
@@ -223,5 +223,15 @@
</intent-filter>
</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>
</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.content.*;
import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.tasks.*;
@@ -38,7 +38,7 @@ import java.io.*;
*/
public class HabitsApplication extends Application
{
private static Context context;
private Context context;
private static AppComponent component;
@@ -58,26 +58,14 @@ public class HabitsApplication extends Application
HabitsApplication.component = component;
}
@NonNull
@Deprecated
public static Context getContext()
{
if (context == null) throw new RuntimeException("context is null");
return context;
}
public static boolean isTestMode()
{
try
{
if (context != null)
{
String testClass = "org.isoron.uhabits.BaseAndroidTest";
context.getClassLoader().loadClass(testClass);
}
Class.forName ("org.isoron.uhabits.BaseAndroidTest");
return true;
}
catch (final Exception e)
catch (final ClassNotFoundException e)
{
return false;
}
@@ -87,7 +75,7 @@ public class HabitsApplication extends Application
public void onCreate()
{
super.onCreate();
HabitsApplication.context = this;
context = this;
component = DaggerAppComponent
.builder()
@@ -96,11 +84,20 @@ public class HabitsApplication extends Application
if (isTestMode())
{
File db = DatabaseUtils.getDatabaseFile();
File db = DatabaseUtils.getDatabaseFile(context);
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.startListening();
@@ -125,7 +122,7 @@ public class HabitsApplication extends Application
@Override
public void onTerminate()
{
HabitsApplication.context = null;
context = null;
ActiveAndroid.dispose();
reminderScheduler.stopListening();

View File

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

View File

@@ -70,7 +70,7 @@ public class BaseSystem
if (context == null) throw new RuntimeException(
"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");
File logFile =

View File

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

View File

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

View File

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

@@ -256,7 +256,7 @@ public class FrequencyChart extends ScrollableChart
float scale = 1.0f/maxFreq * value;
float radius = maxRadius * scale;
int colorIndex = Math.round((colors.length-1) * scale);
int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale));
pGraph.setColor(colors[colorIndex]);
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
}

View File

@@ -32,7 +32,8 @@ import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static org.isoron.uhabits.models.Checkmark.*;
import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY;
import static org.isoron.uhabits.models.Checkmark.UNCHECKED;
public class HistoryChart extends ScrollableChart
{
@@ -112,10 +113,21 @@ public class HistoryChart extends ScrollableChart
if (!isEditable) return false;
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
float x, y;
int pointerId = e.getPointerId(0);
float x = e.getX(pointerId);
float y = e.getY(pointerId);
try
{
int pointerId = e.getPointerId(0);
x = e.getX(pointerId);
y = e.getY(pointerId);
}
catch (RuntimeException ex)
{
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false;
}
final Long timestamp = positionToTimestamp(x, y);
if (timestamp == null) return false;

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.common.views;
import android.animation.*;
import android.content.*;
import android.os.*;
import android.util.*;
import android.view.*;
import android.widget.*;
@@ -32,7 +33,9 @@ public abstract class ScrollableChart extends View
private int dataOffset;
private int scrollerBucketSize;
private int scrollerBucketSize = 1;
private int direction = 1;
private GestureDetector detector;
@@ -40,6 +43,10 @@ public abstract class ScrollableChart extends View
private ValueAnimator scrollAnimator;
private ScrollController scrollController;
private int maxDataOffset = 10000;
public ScrollableChart(Context context)
{
super(context);
@@ -63,8 +70,7 @@ public abstract class ScrollableChart extends View
if (!scroller.isFinished())
{
scroller.computeScrollOffset();
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
postInvalidate();
updateDataOffset();
}
else
{
@@ -85,19 +91,50 @@ public abstract class ScrollableChart extends View
float velocityY)
{
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
(int) velocityX / 2, 0, 0, 100000, 0, 0);
direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
invalidate();
scrollAnimator.setDuration(scroller.getDuration());
scrollAnimator.start();
return false;
}
@Override
public void onLongPress(MotionEvent e)
private int getMaxX()
{
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
@@ -111,12 +148,14 @@ public abstract class ScrollableChart extends View
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;
}
@@ -138,6 +177,32 @@ public abstract class ScrollableChart extends View
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)
{
this.scrollerBucketSize = scrollerBucketSize;
@@ -149,5 +214,25 @@ public abstract class ScrollableChart extends View
scroller = new Scroller(context, null, true);
scrollAnimator = ValueAnimator.ofFloat(0, 1);
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 org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.commands.*;
@@ -38,6 +39,8 @@ import java.util.*;
import butterknife.*;
import static org.isoron.uhabits.activities.ThemeSwitcher.*;
public abstract class BaseDialog extends AppCompatDialogFragment
{
@Nullable
@@ -61,6 +64,18 @@ public abstract class BaseDialog extends AppCompatDialogFragment
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
public void onActivityCreated(Bundle savedInstanceState)
{

View File

@@ -24,7 +24,7 @@ import android.support.v4.app.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*;
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.habits.list.model.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
/**
* 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 MidnightTimer midnightTimer;
public ListHabitsComponent getListHabitsComponent()
{
return component;
@@ -77,6 +80,8 @@ public class ListHabitsActivity extends BaseActivity
screen.setSelectionMenu(selectionMenu);
rootView.setController(controller, selectionMenu);
midnightTimer = component.getMidnightTimer();
setScreen(screen);
controller.onStartup();
}
@@ -84,6 +89,7 @@ public class ListHabitsActivity extends BaseActivity
@Override
protected void onPause()
{
midnightTimer.onPause();
screen.onDettached();
adapter.cancelRefresh();
super.onPause();
@@ -95,6 +101,7 @@ public class ListHabitsActivity extends BaseActivity
adapter.refresh();
screen.onAttached();
rootView.postInvalidate();
midnightTimer.onResume();
if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
prefs.isPureBlackEnabled() != pureBlack)

View File

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

View File

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

View File

@@ -112,6 +112,22 @@ public class ListHabitsMenu extends BaseMenu
invalidate();
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:
return false;
}

View File

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

View File

@@ -19,7 +19,9 @@
package org.isoron.uhabits.activities.habits.list;
import android.app.*;
import android.content.*;
import android.net.*;
import android.support.annotation.*;
import org.isoron.uhabits.*;
@@ -31,24 +33,32 @@ import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import javax.inject.*;
import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*;
@ActivityScope
public class ListHabitsScreen extends BaseScreen
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_DB = 3;
public static final int RESULT_BUG_REPORT = 4;
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
private ListHabitsController controller;
@@ -125,6 +135,15 @@ public class ListHabitsScreen extends BaseScreen
@Override
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;
@@ -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)
{
this.controller = controller;
@@ -208,6 +251,21 @@ public class ListHabitsScreen extends BaseScreen
}
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);
@@ -220,7 +278,8 @@ public class ListHabitsScreen extends BaseScreen
FilePickerDialog picker = filePickerDialogFactory.create(dir);
if (controller != null)
picker.setListener(file -> controller.onImportData(file));
picker.setListener(file -> controller.onImportData(file, () -> {}));
activity.showDialog(picker.getDialog());
}
@@ -233,7 +292,7 @@ public class ListHabitsScreen extends BaseScreen
public void showSettingsScreen()
{
Intent intent = intentFactory.startSettingsActivity(activity);
activity.startActivityForResult(intent, 0);
activity.startActivityForResult(intent, REQUEST_SETTINGS);
}
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.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
@@ -41,7 +43,7 @@ import javax.inject.*;
@ActivityScope
public class HabitCardListAdapter
extends RecyclerView.Adapter<HabitCardViewHolder>
implements HabitCardListCache.Listener
implements HabitCardListCache.Listener, MidnightTimer.MidnightListener
{
@NonNull
private ModelObservable observable;
@@ -55,19 +57,36 @@ public class HabitCardListAdapter
@NonNull
private final HabitCardListCache cache;
@NonNull
private Preferences preferences;
private final MidnightTimer midnightTimer;
@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.observable = new ModelObservable();
this.cache = cache;
this.midnightTimer = midnightTimer;
cache.setListener(this);
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
cache.setOrder(preferences.getDefaultOrder());
setHasStableIds(true);
}
@Override
public void atMidnight()
{
cache.refreshAllHabits();
}
public void cancelRefresh()
{
cache.cancelTasks();
@@ -86,11 +105,10 @@ public class HabitCardListAdapter
* Returns the item that occupies a certain position on the list
*
* @param position position of the item
* @return the item at given position
* @throws IndexOutOfBoundsException if position is not valid
* @return the item at given position or null if position is invalid
*/
@Deprecated
@NonNull
@Nullable
public Habit getItem(int position)
{
return cache.getHabitByPosition(position);
@@ -130,12 +148,18 @@ public class HabitCardListAdapter
return selected.isEmpty();
}
public boolean isSortable()
{
return cache.getOrder() == HabitList.Order.BY_POSITION;
}
/**
* Notify the adapter that it has been attached to a ListView.
*/
public void onAttached()
{
cache.onAttached();
midnightTimer.addListener(this);
}
@Override
@@ -153,6 +177,20 @@ public class HabitCardListAdapter
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
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
int viewType)
@@ -168,6 +206,7 @@ public class HabitCardListAdapter
public void onDetached()
{
cache.onDetached();
midnightTimer.removeListener(this);
}
@Override
@@ -260,6 +299,12 @@ public class HabitCardListAdapter
this.listView = listView;
}
public void setOrder(HabitList.Order order)
{
cache.setOrder(order);
preferences.setDefaultOrder(order);
}
/**
* Selects or deselects the item at a given position.
*
@@ -268,6 +313,8 @@ public class HabitCardListAdapter
public void toggleSelection(int position)
{
Habit h = getItem(position);
if (h == null) return;
int k = selected.indexOf(h);
if (k < 0) selected.add(h);
else selected.remove(h);

View File

@@ -93,12 +93,12 @@ public class HabitCardListCache implements CommandRunner.Listener
* Returns the habits that occupies a certain position on the list.
*
* @param position the position of the habit
* @return the habit at given position
* @throws IndexOutOfBoundsException if position is not valid
* @return the habit at given position or null if position is invalid
*/
@NonNull
public Habit getHabitByPosition(int position)
@Nullable
public synchronized Habit getHabitByPosition(int position)
{
if(position < 0 || position >= data.habits.size()) return null;
return data.habits.get(position);
}
@@ -107,6 +107,11 @@ public class HabitCardListCache implements CommandRunner.Listener
return data.habits.size();
}
public HabitList.Order getOrder()
{
return filteredHabits.getOrder();
}
public int getScore(long habitId)
{
return data.scores.get(habitId);
@@ -180,6 +185,13 @@ public class HabitCardListCache implements CommandRunner.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
* cache has been modified.

View File

@@ -20,21 +20,34 @@
package org.isoron.uhabits.activities.habits.list.views;
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.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
public class CheckmarkButtonView extends TextView
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 value;
private StyledResources res;
private StyledResources styledRes;
private TextPaint paint;
private int lowContrastColor;
private RectF rect;
public CheckmarkButtonView(Context context)
{
@@ -42,6 +55,21 @@ public class CheckmarkButtonView extends TextView
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)
{
this.color = color;
@@ -57,55 +85,60 @@ public class CheckmarkButtonView extends TextView
public void setValue(int value)
{
this.value = value;
updateText();
postInvalidate();
}
public void toggle()
{
value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED :
Checkmark.CHECKED_EXPLICITLY);
value = (value == CHECKED_EXPLICITLY ? UNCHECKED : CHECKED_EXPLICITLY);
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()
{
res = new StyledResources(getContext());
setWillNotDraw(false);
setHapticFeedbackEnabled(false);
setMinHeight(
getResources().getDimensionPixelSize(R.dimen.checkmarkHeight));
setMinWidth(
getResources().getDimensionPixelSize(R.dimen.checkmarkWidth));
setFocusable(false);
setGravity(Gravity.CENTER);
setTypeface(InterfaceUtils.getFontAwesome(getContext()));
}
private void updateText()
{
int lowContrastColor = res.getColor(R.attr.lowContrastTextColor);
Resources res = getResources();
styledRes = new StyledResources(getContext());
if (value == Checkmark.CHECKED_EXPLICITLY)
{
setText(R.string.fa_check);
setTextColor(color);
}
paint = new TextPaint();
paint.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(res.getDimension(R.dimen.regularTextSize));
if (value == Checkmark.CHECKED_IMPLICITLY)
{
setText(R.string.fa_check);
setTextColor(lowContrastColor);
}
if (value == Checkmark.UNCHECKED)
{
setText(R.string.fa_times);
setTextColor(lowContrastColor);
}
rect = new RectF();
color = ColorUtils.getAndroidTestColor(0);
lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
}
}

View File

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

View File

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

View File

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

View File

@@ -20,30 +20,39 @@
package org.isoron.uhabits.activities.habits.list.views;
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.widget.*;
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.utils.*;
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;
@Nullable
private Preferences prefs;
@Nullable
private MidnightTimer midnightTimer;
private final TextPaint paint;
private RectF rect;
public HeaderView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.context = context;
if (isInEditMode())
{
@@ -56,51 +65,116 @@ public class HeaderView extends LinearLayout implements Preferences.Listener
HabitsApplication app = (HabitsApplication) appContext;
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
public void onCheckmarkOrderChanged()
{
createButtons();
updateDirection();
postInvalidate();
}
public void setButtonCount(int buttonCount)
{
this.buttonCount = buttonCount;
createButtons();
postInvalidate();
}
@Override
protected void onAttachedToWindow()
{
updateDirection();
super.onAttachedToWindow();
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
protected void onDetachedFromWindow()
{
if (midnightTimer != null) midnightTimer.removeListener(this);
if (prefs != null) prefs.removeListener(this);
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();
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++)
addView(
inflate(context, R.layout.list_habits_header_checkmark, null));
for (int i = 0; i < getChildCount(); i++)
{
int position = i;
if (shouldReverseCheckmarks()) position = getChildCount() - i - 1;
rect.set(0, 0, width, height);
rect.offset(canvas.getWidth(), 0);
View button = getChildAt(position);
TextView label = (TextView) button.findViewById(R.id.tvCheck);
label.setText(DateUtils.formatHeaderDate(day));
if(reverse) rect.offset(- (i + 1) * width, 0);
else rect.offset((i - buttonCount) * width, 0);
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);
}
}

View File

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

View File

@@ -24,6 +24,10 @@ import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import java.util.*;
import javax.inject.*;
@@ -33,12 +37,38 @@ public class ShowHabitsMenu extends BaseMenu
@NonNull
private final ShowHabitScreen screen;
@NonNull
private final Habit habit;
@NonNull
private final TaskRunner taskRunner;
@NonNull
private ExportCSVTaskFactory exportCSVFactory;
@Inject
public ShowHabitsMenu(@NonNull BaseActivity activity,
@NonNull ShowHabitScreen screen)
@NonNull ShowHabitScreen screen,
@NonNull Habit habit,
@NonNull ExportCSVTaskFactory exportCSVFactory,
@NonNull TaskRunner taskRunner)
{
super(activity);
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
@@ -50,6 +80,10 @@ public class ShowHabitsMenu extends BaseMenu
screen.showEditHabitDialog();
return true;
case R.id.export:
this.exportHabit();
return true;
default:
return false;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,9 +61,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT);
updateRingtoneDescription();
if (InterfaceUtils.isLocaleFullyTranslated())
removePreference("translate", "linksCategory");
}
@Override
@@ -110,14 +107,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
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)
{
Preference pref = findPreference(key);

View File

@@ -25,7 +25,7 @@ import android.support.v7.widget.*;
import android.support.v7.widget.Toolbar;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
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)
{
String url = context.getString(R.string.playStoreURL);

View File

@@ -19,18 +19,20 @@
package org.isoron.uhabits.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.content.*;
import android.database.*;
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.utils.DatabaseUtils;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.*;
import java.io.File;
import java.io.IOException;
import java.io.*;
import javax.inject.*;
@@ -39,10 +41,15 @@ import javax.inject.*;
*/
public class LoopDBImporter extends AbstractImporter
{
@NonNull
private Context context;
@Inject
public LoopDBImporter(@NonNull HabitList habits)
public LoopDBImporter(@NonNull @AppContext Context context,
@NonNull HabitList habits)
{
super(habits);
this.context = context;
}
@Override
@@ -53,23 +60,37 @@ public class LoopDBImporter extends AbstractImporter
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
boolean canHandle = true;
Cursor c = db.rawQuery(
"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();
db.close();
return result;
return canHandle;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
ActiveAndroid.dispose();
File originalDB = DatabaseUtils.getDatabaseFile();
File originalDB = DatabaseUtils.getDatabaseFile(context);
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
*/
public final int getTodayValue()
public int getTodayValue()
{
Checkmark today = getToday();
if (today != null) return today.getValue();
@@ -192,7 +192,7 @@ public abstract class CheckmarkList
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if (newest == null)
if (newest == null || oldest == null)
{
forceRecompute(from, to);
}
@@ -208,6 +208,7 @@ public abstract class CheckmarkList
*
* @return oldest checkmark already computed
*/
@Nullable
protected abstract Checkmark getOldestComputed();
/**
@@ -285,5 +286,6 @@ public abstract class CheckmarkList
*
* @return newest checkmark already computed
*/
@Nullable
protected abstract Checkmark getNewestComputed();
}

View File

@@ -48,9 +48,7 @@ public abstract class HabitList implements Iterable<Habit>
public HabitList()
{
observable = new ModelObservable();
filter = new HabitMatcherBuilder()
.setArchivedAllowed(true)
.build();
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
}
protected HabitList(@NonNull HabitMatcher filter)
@@ -106,6 +104,15 @@ public abstract class HabitList implements Iterable<Habit>
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
* not contain the habit.
@@ -149,7 +156,7 @@ public abstract class HabitList implements Iterable<Habit>
public void repair()
{
for(Habit h : this)
for (Habit h : this)
{
h.getCheckmarks().invalidateNewerThan(0);
h.getStreaks().invalidateNewerThan(0);
@@ -228,4 +235,12 @@ public abstract class HabitList implements Iterable<Habit>
csv.close();
}
public enum Order
{
BY_NAME,
BY_COLOR,
BY_SCORE,
BY_POSITION
}
}

View File

@@ -81,7 +81,7 @@ public abstract class ScoreList implements Iterable<Score>
* @param timestamp the timestamp of a day
* @return score value for that day
*/
public final int getValue(long timestamp)
public synchronized final int getValue(long timestamp)
{
compute(timestamp, timestamp);
Score s = getComputedByTimestamp(timestamp);

View File

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

View File

@@ -25,6 +25,8 @@ import org.isoron.uhabits.models.*;
import java.util.*;
import static org.isoron.uhabits.models.HabitList.Order.*;
/**
* In-memory implementation of {@link HabitList}.
*/
@@ -33,16 +35,23 @@ public class MemoryHabitList extends HabitList
@NonNull
private LinkedList<Habit> list;
private Comparator<Habit> comparator = null;
@NonNull
private Order order;
public MemoryHabitList()
{
super();
list = new LinkedList<>();
order = Order.BY_POSITION;
}
protected MemoryHabitList(@NonNull HabitMatcher matcher)
{
super(matcher);
list = new LinkedList<>();
order = Order.BY_POSITION;
}
@Override
@@ -57,6 +66,7 @@ public class MemoryHabitList extends HabitList
if (id == null) habit.setId((long) list.size());
list.addLast(habit);
resort();
}
@Override
@@ -82,10 +92,17 @@ public class MemoryHabitList extends HabitList
public HabitList getFiltered(HabitMatcher 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;
}
@Override
public Order getOrder()
{
return order;
}
@Override
public int indexOf(@NonNull Habit h)
{
@@ -112,6 +129,14 @@ public class MemoryHabitList extends HabitList
list.add(toPos, from);
}
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override
public int size()
{
@@ -123,4 +148,34 @@ public class MemoryHabitList extends HabitList
{
// 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 com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
@@ -38,12 +37,22 @@ import java.util.*;
*/
public class SQLiteCheckmarkList extends CheckmarkList
{
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 >= ?";
@Nullable
private HabitRecord habitRecord;
@NonNull
private final SQLiteUtils<CheckmarkRecord> sqlite;
@Nullable
private CachedData cache;
public SQLiteCheckmarkList(Habit habit)
{
super(habit);
@@ -54,16 +63,11 @@ public class SQLiteCheckmarkList extends CheckmarkList
public void add(List<Checkmark> checkmarks)
{
check(habit.getId());
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(ADD_QUERY);
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Checkmark c : checkmarks)
{
statement.bindLong(1, habit.getId());
@@ -87,8 +91,7 @@ public class SQLiteCheckmarkList extends CheckmarkList
check(habit.getId());
compute(fromTimestamp, toTimestamp);
String query = "select habit, timestamp, value " +
"from checkmarks " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"order by timestamp desc";
@@ -101,26 +104,28 @@ public class SQLiteCheckmarkList extends CheckmarkList
List<CheckmarkRecord> records = sqlite.query(query, params);
for (CheckmarkRecord record : records) record.habit = habitRecord;
int nDays = DateUtils.getDaysBetween(fromTimestamp, toTimestamp) + 1;
if (records.size() != nDays)
{
throw new InconsistentDatabaseException(
String.format("habit=%s, %d expected, %d found",
habit.getName(), nDays, records.size()));
}
records = fixRecords(records, habitRecord, fromTimestamp, toTimestamp);
return toCheckmarks(records);
}
@Override
public int getTodayValue()
{
if (cache == null || cache.expired())
cache = new CachedData(super.getTodayValue());
return cache.todayValue;
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
cache = null;
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamp);
statement.execute();
observable.notifyListeners();
}
@@ -129,10 +134,8 @@ public class SQLiteCheckmarkList extends CheckmarkList
protected Checkmark getNewestComputed()
{
check(habit.getId());
String query = "select habit, timestamp, value " +
"from checkmarks " +
"where habit = ? " +
"order by timestamp desc " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? " + "order by timestamp desc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
@@ -140,13 +143,12 @@ public class SQLiteCheckmarkList extends CheckmarkList
}
@Override
@Nullable
protected Checkmark getOldestComputed()
{
check(habit.getId());
String query = "select habit, timestamp, value " +
"from checkmarks " +
"where habit = ? " +
"order by timestamp asc " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? " + "order by timestamp asc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
@@ -179,4 +181,44 @@ public class SQLiteCheckmarkList extends CheckmarkList
for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark());
return checkmarks;
}
public static List<CheckmarkRecord> fixRecords(List<CheckmarkRecord> original,
HabitRecord habit,
long fromTimestamp,
long toTimestamp)
{
long day = DateUtils.millisecondsInOneDay;
ArrayList<CheckmarkRecord> records = new ArrayList<>();
for (long t = toTimestamp; t >= fromTimestamp; t -= day)
records.add(new CheckmarkRecord(habit, t, Checkmark.UNCHECKED));
for (CheckmarkRecord record : original)
{
if ((toTimestamp - record.timestamp) % day != 0) continue;
int offset = (int) ((toTimestamp - record.timestamp) / day);
if (offset < 0 || offset >= records.size()) continue;
records.set(offset, record);
}
return records;
}
private static class CachedData
{
int todayValue;
private long today;
CachedData(int todayValue)
{
this.todayValue = todayValue;
this.today = DateUtils.getStartOfToday();
}
boolean expired()
{
return today != DateUtils.getStartOfToday();
}
}
}

View File

@@ -39,10 +39,15 @@ public class SQLiteHabitList extends HabitList
private static SQLiteHabitList instance;
@NonNull
private final SQLiteUtils<HabitRecord> sqlite;
@NonNull
private final ModelFactory modelFactory;
@NonNull
private Order order;
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{
super();
@@ -50,16 +55,19 @@ public class SQLiteHabitList extends HabitList
if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class);
order = Order.BY_POSITION;
}
protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
@NonNull HabitMatcher filter)
@NonNull HabitMatcher filter,
@NonNull Order order)
{
super(filter);
this.modelFactory = modelFactory;
if (cache == null) cache = new HashMap<>();
sqlite = new SQLiteUtils<>(HabitRecord.class);
this.order = order;
}
public static SQLiteHabitList getInstance(
@@ -118,7 +126,20 @@ public class SQLiteHabitList extends HabitList
@Override
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
@@ -214,6 +235,13 @@ public class SQLiteHabitList extends HabitList
getObservable().notifyListeners();
}
@Override
public void repair()
{
super.repair();
rebuildOrder();
}
@Override
public int size()
{
@@ -233,7 +261,7 @@ public class SQLiteHabitList extends HabitList
}
}
protected List<Habit> toList()
protected synchronized List<Habit> toList()
{
String query = buildSelectQuery();
List<HabitRecord> recordList = sqlite.query(query, null);
@@ -242,19 +270,43 @@ public class SQLiteHabitList extends HabitList
for (HabitRecord record : recordList)
{
Habit habit = getById(record.getId());
if (habit == null)
throw new RuntimeException("habit not in database");
if (habit == null) continue;
if (!filter.matches(habit)) continue;
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;
}
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)
@@ -282,11 +334,4 @@ public class SQLiteHabitList extends HabitList
appendOrderBy(query);
return query.toString();
}
@Override
public void repair()
{
super.repair();
rebuildOrder();
}
}

View File

@@ -19,12 +19,12 @@
package org.isoron.uhabits.models.sqlite;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.*;
import android.database.sqlite.*;
import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*;
@@ -43,10 +43,16 @@ public class SQLiteRepetitionList extends RepetitionList
@Nullable
private HabitRecord habitRecord;
private SQLiteStatement addStatement;
public SQLiteRepetitionList(@NonNull Habit habit)
{
super(habit);
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)
{
check(habit.getId());
RepetitionRecord record = new RepetitionRecord();
record.copyFrom(rep);
record.habit = habitRecord;
record.save();
addStatement.bindLong(1, habit.getId());
addStatement.bindLong(2, rep.getTimestamp());
addStatement.execute();
observable.notifyListeners();
}

View File

@@ -24,10 +24,10 @@ import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*;
import java.util.*;
@@ -37,12 +37,21 @@ import java.util.*;
*/
public class SQLiteScoreList extends ScoreList
{
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 >= ?";
@Nullable
private HabitRecord habitRecord;
@NonNull
private final SQLiteUtils<ScoreRecord> sqlite;
@Nullable
private CachedData cache = null;
/**
* Constructs a new ScoreList associated with the given habit.
*
@@ -58,16 +67,11 @@ public class SQLiteScoreList extends ScoreList
public void add(List<Score> scores)
{
check(habit.getId());
String query =
"insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(ADD_QUERY);
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Score s : scores)
{
statement.bindLong(1, habit.getId());
@@ -86,20 +90,20 @@ public class SQLiteScoreList extends ScoreList
@NonNull
@Override
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
public synchronized List<Score> getByInterval(long fromTimestamp,
long toTimestamp)
{
check(habit.getId());
compute(fromTimestamp, toTimestamp);
String query = "select habit, timestamp, score " +
"from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"order by timestamp desc";
String query = "select habit, timestamp, score from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"order by timestamp desc";
String params[] = {
Long.toString(habit.getId()),
Long.toString(fromTimestamp),
Long.toString(toTimestamp)
Long.toString(habit.getId()),
Long.toString(fromTimestamp),
Long.toString(toTimestamp)
};
List<ScoreRecord> records = sqlite.query(query, params);
@@ -124,14 +128,23 @@ public class SQLiteScoreList extends ScoreList
}
@Override
public void invalidateNewerThan(long timestamp)
public synchronized int getTodayValue()
{
new Delete()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
if (cache == null || cache.expired())
cache = new CachedData(super.getTodayValue());
return cache.todayValue;
}
@Override
public synchronized void invalidateNewerThan(long timestamp)
{
cache = null;
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamp);
statement.execute();
getObservable().notifyListeners();
}
@@ -159,8 +172,7 @@ public class SQLiteScoreList extends ScoreList
{
check(habit.getId());
String query = "select habit, timestamp, score from Score " +
"where habit = ? order by timestamp desc " +
"limit 1";
"where habit = ? order by timestamp desc limit 1";
String params[] = { Long.toString(habit.getId()) };
return getScoreFromQuery(query, params);
@@ -172,8 +184,7 @@ public class SQLiteScoreList extends ScoreList
{
check(habit.getId());
String query = "select habit, timestamp, score from Score " +
"where habit = ? order by timestamp asc " +
"limit 1";
"where habit = ? order by timestamp asc limit 1";
String params[] = { Long.toString(habit.getId()) };
return getScoreFromQuery(query, params);
@@ -204,4 +215,22 @@ public class SQLiteScoreList extends ScoreList
for (ScoreRecord r : records) scores.add(r.toScore());
return scores;
}
private static class CachedData
{
int todayValue;
private long today;
CachedData(int todayValue)
{
this.todayValue = todayValue;
this.today = DateUtils.getStartOfToday();
}
boolean expired()
{
return today != DateUtils.getStartOfToday();
}
}
}

View File

@@ -19,10 +19,11 @@
package org.isoron.uhabits.models.sqlite;
import android.database.sqlite.*;
import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.query.*;
import com.activeandroid.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
@@ -36,6 +37,10 @@ import java.util.*;
*/
public class SQLiteStreakList extends StreakList
{
private static final String INVALIDATE_QUERY =
"delete from Streak where habit = ? and end >= ?";
private HabitRecord habitRecord;
@NonNull
@@ -73,12 +78,11 @@ public class SQLiteStreakList extends StreakList
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamp - DateUtils.millisecondsInOneDay);
statement.execute();
observable.notifyListeners();
}

View File

@@ -24,6 +24,7 @@ import android.database.*;
import com.activeandroid.*;
import com.activeandroid.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.models.*;
/**
@@ -53,6 +54,17 @@ public class CheckmarkRecord extends Model implements SQLiteRecord
@Column(name = "value")
public Integer value;
public CheckmarkRecord()
{
}
public CheckmarkRecord(HabitRecord habit, Long timestamp, Integer value)
{
this.habit = habit;
this.timestamp = timestamp;
this.value = value;
}
@Override
public void copyFrom(Cursor c)
{
@@ -64,4 +76,40 @@ public class CheckmarkRecord extends Model implements SQLiteRecord
{
return new Checkmark(timestamp, value);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CheckmarkRecord that = (CheckmarkRecord) o;
return new EqualsBuilder()
.append(habit, that.habit)
.append(timestamp, that.timestamp)
.append(value, that.value)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(habit)
.append(timestamp)
.append(value)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("habit", habit)
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

View File

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

View File

@@ -24,6 +24,7 @@ import android.preference.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
import java.util.*;
@@ -61,6 +62,21 @@ public class Preferences
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()
{
int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1);
@@ -69,6 +85,11 @@ public class Preferences
return defaultScoreInterval;
}
public void setDefaultOrder(HabitList.Order order)
{
prefs.edit().putString("pref_default_order", order.name()).apply();
}
public void setDefaultScoreSpinnerPosition(int position)
{
prefs.edit().putInt("pref_score_view_interval", position).apply();

View File

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

View File

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

View File

@@ -28,6 +28,8 @@ import org.isoron.uhabits.utils.*;
import javax.inject.*;
import static org.isoron.uhabits.utils.DateUtils.*;
@ReceiverScope
public class ReminderController
{
@@ -66,7 +68,7 @@ public class ReminderController
{
long snoozeInterval = preferences.getSnoozeInterval();
long now = DateUtils.getLocalTime();
long now = applyTimezone(getLocalTime());
long reminderTime = now + snoozeInterval * 60 * 1000;
reminderScheduler.schedule(habit, reminderTime);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,19 +22,12 @@ package org.isoron.uhabits.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.v4.view.*;
import android.util.*;
import java.util.*;
import android.view.*;
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;
public static Typeface getFontAwesome(Context context)
@@ -59,14 +52,9 @@ public abstract class InterfaceUtils
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();
for(String lang : fullyTranslatedLanguages)
if(currentLanguage.equals(lang)) return true;
return false;
return ViewCompat.getLayoutDirection(view) ==
ViewCompat.LAYOUT_DIRECTION_RTL;
}
}

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.widget.*;
import com.activeandroid.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
@@ -76,8 +78,15 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
for (int id : ids)
{
BaseWidget widget = getWidgetFromId(context, id);
widget.delete();
try
{
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"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar">
@@ -86,6 +87,11 @@
style="@style/About.Item.Clickable"
android:text="@string/pref_send_feedback"/>
<TextView
android:id="@+id/tvTranslate"
style="@style/About.Item.Clickable"
android:text="@string/help_translate"/>
<TextView
android:id="@+id/tvSource"
style="@style/About.Item.Clickable"
@@ -117,6 +123,9 @@
<TextView
style="@style/About.Item"
android:text="Nikhil (regularcoder)"/>
<TextView
style="@style/About.Item"
android:text="JanetQC"/>
</LinearLayout>
@@ -129,17 +138,41 @@
android:text="@string/translators"
android:textColor="?aboutScreenColor"/>
<TextView
style="@style/About.Item"
android:text="Mihail Stefanov (Bǎlgarski)"/>
<TextView
style="@style/About.Item"
android:text="Angga Rifandi (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="raden20 (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="azzamsa (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="David Nos (Català)"/>
<TextView
style="@style/About.Item"
android:text="Tomáš Borovec (Čeština)"/>
<TextView
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
style="@style/About.Item"
@@ -157,6 +190,22 @@
style="@style/About.Item"
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
style="@style/About.Item"
android:text="François Mahé (Français)"/>
@@ -189,6 +238,10 @@
style="@style/About.Item"
android:text="Jelle den Butter (Nederlands)"/>
<TextView
style="@style/About.Item"
android:text="nitovf9292 (Norsk)"/>
<TextView
style="@style/About.Item"
android:text="Adam Jurkiewicz (Polski)"/>
@@ -205,13 +258,17 @@
style="@style/About.Item"
android:text="Dmitriy Bogdanov (Русский)"/>
<TextView
style="@style/About.Item"
android:text="Andrei Pleș (Română)"/>
<TextView
style="@style/About.Item"
android:text="Dušan Strgar (Slovenščina)"/>
<TextView
style="@style/About.Item"
android:text="Dalecarlian (Svenska)"/>
android:text="Alexander Jansson (Svenska)"/>
<TextView
style="@style/About.Item"
@@ -233,6 +290,10 @@
style="@style/About.Item"
android:text="Rystard (Українська)"/>
<TextView
style="@style/About.Item"
android:text="Oglaigh Rystard (Українська)"/>
<TextView
style="@style/About.Item"
android:text="Limin Lu (中文)"/>
@@ -269,13 +330,26 @@
style="@style/About.Item"
android:text="Josh Graham (한국어 )"/>
<TextView
style="@style/About.Item"
android:text="Seoyul (한국어 )"/>
<TextView
style="@style/About.Item"
android:text="Aman Satnami (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Andreas Michelakis (Ελληνικά)"/>
android:text="Niraj Yadav (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Yoav Argov (עברית‎)"/>
<TextView
style="@style/About.Item"
android:text="Mahdi Nasiri (فارسی‎)"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

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

View File

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

View File

@@ -45,6 +45,26 @@
android:checkable="true"
android:enabled="true"
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>
</item>
@@ -73,5 +93,4 @@
android:orderInCategory="100"
android:title="@string/about"
app:showAsAction="never"/>
</menu>

View File

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

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