Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf3964a231 | |||
| 657cde75d8 | |||
| c56b86d32c | |||
| 16f20d50a0 | |||
| 7bb88dcb97 | |||
| 7bb62c197f | |||
| 540a618ba8 | |||
| bf24cc608c | |||
| a73459784e | |||
| de3b97dfdf | |||
| 91996924d9 | |||
| 9fe446b424 | |||
| 3857eaf5e9 | |||
| e29fb58922 | |||
| 404fc869b0 | |||
| 001dd5a7c1 | |||
| 7930cc8f31 | |||
| 526830ba61 | |||
| fe1513bb64 | |||
| e06ace9ea8 | |||
| d727dabb2b | |||
| d17e8fcbfb | |||
| be3d7145ab | |||
| cf66587644 | |||
| 0dc9ec2e5f | |||
| 0a375ded96 | |||
| fa5d6f8fee | |||
| 534e6c2d9d | |||
| b6501c9a29 | |||
| 238a1c724d | |||
| 34ca9d17a2 | |||
| e844390614 | |||
| 5e00d07b73 | |||
| 28b6ae7014 | |||
| 2a1bf5fc2e | |||
| ef7483f9dc | |||
| bbb9ed8f99 | |||
| c49d576871 | |||
| bc66ae4f7a | |||
| fa416adbb9 | |||
| 8b835b9918 | |||
| 471c5d341f | |||
| 57296745b3 | |||
| 140ab34a76 | |||
| 0d6ad26505 | |||
| 65cc99dbf7 | |||
| 6855ef9d5e | |||
| 4c58b084c6 | |||
| 5c8e522646 | |||
| 55da0759d4 | |||
| 692fe3218f | |||
| 387930c08d | |||
| 6bd31f9607 | |||
| 9aafe7160c | |||
| 5cc4aac67a | |||
| 831421bc98 | |||
| 161d8f2517 | |||
| bfe4b822b3 | |||
| 19e79a8559 | |||
| 876d4f0ac7 | |||
| f4f7faf3a4 | |||
| 56f2ae57fe | |||
| 3fe09efe9b | |||
| f0de29fbfe | |||
| 324facfffd | |||
| 03e58f9ef2 | |||
| e6c9f7f0c9 | |||
| 42bdedb86a | |||
| ab0c510fda | |||
| e46fd58664 | |||
| 8532bd402e | |||
| 2c599b18ef | |||
| 0d78ba4ba9 | |||
|
|
611dfa00a5 | ||
|
|
54a195243d | ||
|
|
4fc30fae53 | ||
| b3fe9c65d2 | |||
| 09f1ae8765 | |||
| 0a8b763ece | |||
| edd5f25529 | |||
| d81fdb41dc | |||
| 02c8810e46 | |||
| 6adf8061d3 | |||
| d19d57e5df | |||
| fd82e6c24b | |||
| 56263efa39 | |||
| d5eacba303 | |||
| 222261c674 | |||
| b1a06df7f8 | |||
| a1fc7dd0d1 | |||
|
|
10131d5124 | ||
| aa94959ad2 | |||
| 45fd8a29e1 | |||
| 8c4fab28aa |
3
.gitignore
vendored
@@ -20,3 +20,6 @@ captures/
|
||||
docs/
|
||||
gen/
|
||||
local.properties
|
||||
crowdin.yaml
|
||||
local
|
||||
secret/
|
||||
|
||||
42
CHANGELOG.md
@@ -1,5 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
### 1.7.7 (September 30, 2017)
|
||||
|
||||
* Fix bug that caused reminders to show repeatedly on DST changes
|
||||
|
||||
### 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
|
||||
|
||||
@@ -2,30 +2,50 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
apply plugin: 'me.tatarka.retrolambda'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
// signingConfigs {
|
||||
// release {
|
||||
// storeFile file(LOOP_STORE_FILE)
|
||||
// storePassword LOOP_STORE_PASSWORD
|
||||
// keyAlias LOOP_KEY_ALIAS
|
||||
// keyPassword LOOP_KEY_PASSWORD
|
||||
// }
|
||||
// }
|
||||
|
||||
playAccountConfigs {
|
||||
defaultAccountConfig {
|
||||
jsonFile = file('../secret/playstore.json')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.isoron.uhabits"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
targetSdkVersion 25
|
||||
|
||||
buildConfigField "Integer", "databaseVersion", "15"
|
||||
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArgument "size", "medium"
|
||||
|
||||
// playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
// signingConfig signingConfigs.release
|
||||
}
|
||||
debug {
|
||||
testCoverageEnabled = true
|
||||
testCoverageEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +73,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 +84,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'
|
||||
@@ -140,3 +160,7 @@ task coverageReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
|
||||
classDirectories = files(fileTree(dir: classDir, excludes: excludes))
|
||||
executionData = files(jvmExecData, connectedExecData)
|
||||
}
|
||||
|
||||
//play {
|
||||
// track = 'alpha'
|
||||
//}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<manifest
|
||||
package="org.isoron.uhabits"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="25"
|
||||
android:versionName="1.6.2">
|
||||
android:versionCode="35"
|
||||
android:versionName="1.7.8">
|
||||
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ public abstract class DateUtils
|
||||
public static long applyTimezone(long localTimestamp)
|
||||
{
|
||||
TimeZone tz = getTimezone();
|
||||
long now = new Date(localTimestamp).getTime();
|
||||
return now - tz.getOffset(now);
|
||||
return localTimestamp - tz.getOffset(localTimestamp - tz.getOffset(localTimestamp));
|
||||
}
|
||||
|
||||
public static String formatHeaderDate(GregorianCalendar day)
|
||||
@@ -187,6 +186,11 @@ public abstract class DateUtils
|
||||
return getStartOfDay(DateUtils.getLocalTime());
|
||||
}
|
||||
|
||||
public static long millisecondsUntilTomorrow()
|
||||
{
|
||||
return getStartOfToday() + millisecondsInOneDay - getLocalTime();
|
||||
}
|
||||
|
||||
public static GregorianCalendar getStartOfTodayCalendar()
|
||||
{
|
||||
return getCalendar(getStartOfToday());
|
||||
@@ -225,8 +229,7 @@ public abstract class DateUtils
|
||||
public static long removeTimezone(long timestamp)
|
||||
{
|
||||
TimeZone tz = getTimezone();
|
||||
long now = new Date(timestamp).getTime();
|
||||
return now + tz.getOffset(now);
|
||||
return timestamp + tz.getOffset(timestamp);
|
||||
}
|
||||
|
||||
public static void setFixedLocalTime(Long timestamp)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_action_download_dark.png
Normal file
|
After Width: | Height: | Size: 249 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_download_light.png
Normal file
|
After Width: | Height: | Size: 280 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_download_dark.png
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_download_light.png
Normal file
|
After Width: | Height: | Size: 236 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_download_dark.png
Normal file
|
After Width: | Height: | Size: 279 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_download_light.png
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_download_dark.png
Normal file
|
After Width: | Height: | Size: 366 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_download_light.png
Normal file
|
After Width: | Height: | Size: 397 B |
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |