Compare commits

..

67 Commits

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

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.hamcrest.*;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.isoron.uhabits.models.HabitList.Order.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HabitListTest extends BaseAndroidTest
{
private ArrayList<Habit> habitsArray;
private HabitList activeHabits;
private HabitList reminderHabits;
@Override
public void setUp()
{
super.setUp();
habitList.removeAll();
habitsArray = new ArrayList<>();
for (int i = 0; i < 10; i++)
{
Habit habit = fixtures.createEmptyHabit((long) i);
habitsArray.add(habit);
if (i % 3 == 0)
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
habitList.update(habit);
}
habitsArray.get(0).setArchived(true);
habitsArray.get(1).setArchived(true);
habitsArray.get(4).setArchived(true);
habitsArray.get(7).setArchived(true);
activeHabits = habitList.getFiltered(new HabitMatcherBuilder().build());
reminderHabits = habitList.getFiltered(new HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build());
}
@Test
public void test_size()
{
assertThat(habitList.size(), equalTo(10));
}
@Test
public void test_countActive()
{
assertThat(activeHabits.size(), equalTo(6));
}
@Test
public void test_getByPosition()
{
assertThat(habitList.getByPosition(0), equalTo(habitsArray.get(0)));
assertThat(habitList.getByPosition(3), equalTo(habitsArray.get(3)));
assertThat(habitList.getByPosition(9), equalTo(habitsArray.get(9)));
assertThat(activeHabits.getByPosition(0), equalTo(habitsArray.get(2)));
}
@Test
public void test_getHabitsWithReminder()
{
assertThat(reminderHabits.size(), equalTo(4));
assertThat(reminderHabits.getByPosition(1),
equalTo(habitsArray.get(3)));
}
@Test
public void test_get_withInvalidId()
{
assertThat(habitList.getById(100L), is(nullValue()));
}
@Test
public void test_get_withValidId()
{
Habit habit1 = habitsArray.get(0);
Habit habit2 = habitList.getById(habit1.getId());
assertThat(habit1, equalTo(habit2));
}
@Test
public void test_reorder()
{
int operations[][] = {
{ 5, 2 }, { 3, 7 }, { 4, 4 }, { 3, 2 }
};
int expectedPosition[][] = {
{ 0, 1, 3, 4, 5, 2, 6, 7, 8, 9 },
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
{ 0, 1, 7, 2, 4, 3, 5, 6, 8, 9 },
};
for (int i = 0; i < operations.length; i++)
{
int from = operations[i][0];
int to = operations[i][1];
Habit fromHabit = habitList.getByPosition(from);
Habit toHabit = habitList.getByPosition(to);
habitList.reorder(fromHabit, toHabit);
int actualPositions[] = new int[10];
for (int j = 0; j < 10; j++)
{
Habit h = habitList.getById(j);
assertNotNull(h);
actualPositions[j] = habitList.indexOf(h);
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
@Test
public void test_writeCSV() throws IOException
{
habitList.removeAll();
Habit h1 = fixtures.createEmptyHabit();
h1.setName("Meditate");
h1.setDescription("Did you meditate this morning?");
h1.setFrequency(Frequency.DAILY);
h1.setColor(3);
Habit h2 = fixtures.createEmptyHabit();
h2.setName("Wake up early");
h2.setDescription("Did you wake up before 6am?");
h2.setFrequency(new Frequency(2, 3));
h2.setColor(5);
habitList.update(h1);
habitList.update(h2);
String expectedCSV =
"Position,Name,Description,NumRepetitions,Interval,Color\n" +
"001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
StringWriter writer = new StringWriter();
habitList.writeCSV(writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
}
@Test
public void test_ordering()
{
habitList.removeAll();
Habit h3 = fixtures.createEmptyHabit();
h3.setName("C Habit");
h3.setColor(0);
habitList.update(h3);
Habit h1 = fixtures.createEmptyHabit();
h1.setName("A Habit");
h1.setColor(2);
habitList.update(h1);
Habit h4 = fixtures.createEmptyHabit();
h4.setName("D Habit");
h4.setColor(1);
habitList.update(h4);
Habit h2 = fixtures.createEmptyHabit();
h2.setName("B Habit");
h2.setColor(2);
habitList.update(h2);
habitList.setOrder(BY_POSITION);
assertThat(habitList.getByPosition(0), equalTo(h3));
assertThat(habitList.getByPosition(1), equalTo(h1));
assertThat(habitList.getByPosition(2), equalTo(h4));
assertThat(habitList.getByPosition(3), equalTo(h2));
habitList.setOrder(BY_NAME);
assertThat(habitList.getByPosition(0), equalTo(h1));
assertThat(habitList.getByPosition(1), equalTo(h2));
assertThat(habitList.getByPosition(2), equalTo(h3));
assertThat(habitList.getByPosition(3), equalTo(h4));
habitList.remove(h1);
habitList.add(h1);
assertThat(habitList.getByPosition(0), equalTo(h1));
habitList.setOrder(BY_COLOR);
assertThat(habitList.getByPosition(0), equalTo(h3));
assertThat(habitList.getByPosition(1), equalTo(h4));
assertThat(habitList.getByPosition(2), equalTo(h1));
assertThat(habitList.getByPosition(3), equalTo(h2));
}
}

View File

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

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.performance;
import android.support.test.filters.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.junit.*;
@MediumTest
public class PerformanceTest extends BaseAndroidTest
{
private Habit habit;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
}
@Test(timeout = 1000)
public void testRepeatedGetTodayValue()
{
for (int i = 0; i < 100000; i++)
{
habit.getScores().getTodayValue();
habit.getCheckmarks().getTodayValue();
}
}
}

View File

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

View File

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

View File

@@ -21,8 +21,8 @@
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="25"
android:versionName="1.6.2">
android:versionCode="30"
android:versionName="1.7.3">
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.os.*;
import android.support.v4.os.*;
public class BundleSavedState extends android.support.v4.view.AbsSavedState
{
public static final Parcelable.Creator<BundleSavedState> CREATOR =
ParcelableCompat.newCreator(
new ParcelableCompatCreatorCallbacks<BundleSavedState>()
{
@Override
public BundleSavedState createFromParcel(Parcel source,
ClassLoader loader)
{
return new BundleSavedState(source, loader);
}
@Override
public BundleSavedState[] newArray(int size)
{
return new BundleSavedState[size];
}
});
public final Bundle bundle;
public BundleSavedState(Parcelable superState, Bundle bundle)
{
super(superState);
this.bundle = bundle;
}
public BundleSavedState(Parcel source, ClassLoader loader)
{
super(source, loader);
this.bundle = source.readBundle(loader);
}
@Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeBundle(bundle);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,8 @@ import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
@@ -41,7 +43,7 @@ import javax.inject.*;
@ActivityScope
public class HabitCardListAdapter
extends RecyclerView.Adapter<HabitCardViewHolder>
implements HabitCardListCache.Listener
implements HabitCardListCache.Listener, MidnightTimer.MidnightListener
{
@NonNull
private ModelObservable observable;
@@ -55,19 +57,36 @@ public class HabitCardListAdapter
@NonNull
private final HabitCardListCache cache;
@NonNull
private Preferences preferences;
private final MidnightTimer midnightTimer;
@Inject
public HabitCardListAdapter(@NonNull HabitCardListCache cache)
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
@NonNull Preferences preferences,
@NonNull MidnightTimer midnightTimer)
{
this.preferences = preferences;
this.selected = new LinkedList<>();
this.observable = new ModelObservable();
this.cache = cache;
this.midnightTimer = midnightTimer;
cache.setListener(this);
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
cache.setOrder(preferences.getDefaultOrder());
setHasStableIds(true);
}
@Override
public void atMidnight()
{
cache.refreshAllHabits();
}
public void cancelRefresh()
{
cache.cancelTasks();
@@ -130,12 +149,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 +178,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 +207,7 @@ public class HabitCardListAdapter
public void onDetached()
{
cache.onDetached();
midnightTimer.removeListener(this);
}
@Override
@@ -260,6 +300,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.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,10 +39,15 @@ public class SQLiteHabitList extends HabitList
private static SQLiteHabitList instance;
@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);
@@ -249,12 +277,38 @@ public class SQLiteHabitList extends HabitList
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 +336,4 @@ public class SQLiteHabitList extends HabitList
appendOrderBy(query);
return query.toString();
}
@Override
public void repair()
{
super.repair();
rebuildOrder();
}
}

View File

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

View File

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

View File

@@ -19,10 +19,11 @@
package org.isoron.uhabits.models.sqlite;
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.*;
@@ -41,10 +42,17 @@ public class SQLiteStreakList extends StreakList
@NonNull
private final SQLiteUtils<StreakRecord> sqlite;
private final SQLiteStatement invalidateStatement;
public SQLiteStreakList(Habit habit)
{
super(habit);
sqlite = new SQLiteUtils<>(StreakRecord.class);
SQLiteDatabase db = Cache.openDatabase();
String invalidateQuery = "delete from Streak where habit = ? " +
"and end >= ?";
invalidateStatement = db.compileStatement(invalidateQuery);
}
@Override
@@ -73,12 +81,9 @@ 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();
invalidateStatement.bindLong(1, habit.getId());
invalidateStatement.bindLong(2, timestamp - DateUtils.millisecondsInOneDay);
invalidateStatement.execute();
observable.notifyListeners();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils;
import org.isoron.uhabits.activities.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
/**
* A class that emits events when a new day starts.
*/
@ActivityScope
public class MidnightTimer
{
private final List<MidnightListener> listeners;
private ScheduledExecutorService executor;
@Inject
public MidnightTimer()
{
this.listeners = new LinkedList<>();
}
public synchronized void addListener(MidnightListener listener)
{
this.listeners.add(listener);
}
public synchronized void onPause()
{
executor.shutdownNow();
}
public synchronized void onResume()
{
executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> notifyListeners(),
DateUtils.millisecondsUntilTomorrow() + 1000,
DateUtils.millisecondsInOneDay, TimeUnit.MILLISECONDS);
}
public synchronized void removeListener(MidnightListener listener)
{
this.listeners.remove(listener);
}
private synchronized void notifyListeners()
{
for (MidnightListener l : listeners) l.atMidnight();
}
public interface MidnightListener
{
void atMidnight();
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

View File

@@ -31,6 +31,7 @@
style="@style/Toolbar"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar">
@@ -86,6 +87,11 @@
style="@style/About.Item.Clickable"
android:text="@string/pref_send_feedback"/>
<TextView
android:id="@+id/tvTranslate"
style="@style/About.Item.Clickable"
android:text="@string/help_translate"/>
<TextView
android:id="@+id/tvSource"
style="@style/About.Item.Clickable"
@@ -117,6 +123,9 @@
<TextView
style="@style/About.Item"
android:text="Nikhil (regularcoder)"/>
<TextView
style="@style/About.Item"
android:text="JanetQC"/>
</LinearLayout>
@@ -129,17 +138,33 @@
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="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 +182,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)"/>
@@ -205,6 +246,10 @@
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)"/>
@@ -275,7 +320,12 @@
<TextView
style="@style/About.Item"
android:text="Andreas Michelakis (Ελληνικά)"/>
android:text="Niraj Yadav (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Yoav Argov (עברית‎)"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<!-- App introduction -->
<!-- Middle part of the sentence '1 time in xx days' -->
</resources>

View File

@@ -1,190 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<!-- There is no letter P in arabic so the name is unfortunately translated to "Loob" -->
<string name="app_name">"لوب ملاحق العادة "</string>
<string name="main_activity_title">"عادات"</string>
<string name="action_settings">"إعدادات"</string>
<string name="edit">"تعديل"</string>
<string name="delete">"حذف"</string>
<string name="archive">"أرشيف"</string>
<string name="unarchive">"إزالة من الأرشيف"</string>
<string name="add_habit">"إضافة العادة"</string>
<string name="color_picker_default_title">"غير اللون"</string>
<string name="toast_habit_created">"تم صنع عادة "</string>
<string name="toast_habit_deleted">"تم حذف عادة "</string>
<string name="toast_habit_restored">"تم ترجيع عادة"</string>
<string name="toast_nothing_to_undo">"لا شيء للتراجع"</string>
<string name="toast_nothing_to_redo">"لا شيء لتكرار"</string>
<string name="toast_habit_changed">"تم تغييرعادة"</string>
<!-- This appears when the user edits a habit, and then undoes the action. The habit is "changed back" to what is was before. Alternatively, "Habit restored". -->
<string name="toast_habit_changed_back">"تم ترجيع العادة إلى أصلها"</string>
<string name="toast_habit_archived">"تم أرشيف العادات"</string>
<string name="toast_habit_unarchived">"تم إزالة العادة من الأرشيف "</string>
<string name="overview">"نظرة عامة"</string>
<string name="habit_strength">"قوة العادة"</string>
<string name="history">"التاريخ"</string>
<string name="clear">"مسح"</string>
<string name="description_hint">"السؤال (هل ... اليوم؟)"</string>
<!-- This and the next two terms form the sentence "Repeat 3 times in 7 days" that you see when you create a habit. Let me know if you have trouble adapting this into your language. -->
<string name="repeat">"كرر"</string>
<string name="times_every">"مرات في"</string>
<string name="days">"أيام"</string>
<string name="reminder">"تذكير"</string>
<string name="discard">"حذف"</string>
<string name="save">"حفظ"</string>
<!-- Streak as in "winning streak". That is, the number of times a user has performed a habit consecutively. Similar terms are "chains" or "series". -->
<string name="streaks">"تقدم متتالية"</string>
<string name="no_habits_found">" لا يوجد لديك عادات مفعله"</string>
<string name="long_press_to_toggle">"أضغط و إستمر لتحقق أو ازل"</string>
<string name="reminder_off">"أوقف"</string>
<string name="validation_name_should_not_be_blank">"لا يمكن أن يكون الإسم فارغ"</string>
<string name="validation_number_should_be_positive">"يجب أن يكون الرقم إيجابي"</string>
<string name="validation_at_most_one_rep_per_day">"يمكن أن يكون التكرار واحدة فقط كل يوم "</string>
<string name="create_habit">"اخلق عادة"</string>
<string name="edit_habit">"تعديل العادة"</string>
<string name="check">"حقق"</string>
<string name="snooze">"لاحقا"</string>
<!-- App introduction -->
<string name="intro_title_1">"أهلا بك"</string>
<string name="intro_description_1">"لوب يساعدك على خلق والحفاظ على العادات الجيدة."</string>
<string name="intro_title_2">"إنشاء بعض عادات جديدة"</string>
<string name="intro_description_2">"كل يوم، بعد أداء عادتك، وضع علامة على التطبيق."</string>
<string name="intro_title_3">"حافظ على القيام بذلك"</string>
<string name="intro_description_3">"العادة المستمرة لفترات طويلة تكسب نجمة كامله"</string>
<string name="intro_title_4">"تتبع تقدمك"</string>
<string name="intro_description_4">"رسوم بيانية مفصلة تبين لكم كيف تحسن عاداتك مع مرور الوقت."</string>
<string name="interval_15_minutes">"15 دقيقة"</string>
<string name="interval_30_minutes">"30 دقيقة"</string>
<string name="interval_1_hour">"ساعة واحدة"</string>
<string name="interval_2_hour">"ساعتين"</string>
<string name="interval_4_hour">"أربع ساعات"</string>
<string name="interval_8_hour">"ثماني ساعات"</string>
<string name="pref_toggle_title">"تبديل بكبسه"</string>
<string name="pref_toggle_description">"أكثر سهولة، لكنه ممكن يسبب كبسات غير مقصوده"</string>
<string name="pref_snooze_interval_title">"فترتي الغفوى على التذكير"</string>
<string name="pref_rate_this_app">"تقييم هذا التطبيق على جوجل بلاي"</string>
<string name="pref_send_feedback">"أرسل الملاحظات إلى المطور"</string>
<string name="pref_view_source_code">"إفحص التعليمات البرمجية على GitHub"</string>
<string name="pref_view_app_introduction">"عرض المقدمه"</string>
<string name="links">"روابط"</string>
<string name="behavior">"سلوك"</string>
<string name="name">"اسم"</string>
<string name="show_archived">"عرض أرشفة"</string>
<string name="settings">"إعدادات"</string>
<string name="snooze_interval">"فترتي الغفوه"</string>
<string name="hint_title">"هل كنت تعلم؟"</string>
<string name="hint_drag">"لإعادة ترتيب القوائم، أضغط اسم من هذه العادة، ثم اسحبه إلى المكان الصحيح."</string>
<string name="hint_landscape">"يمكنك ان ترى المزيد أيام عن طريق وضع الهاتف في وضع أفقي."</string>
<string name="delete_habits">"حذف عادات"</string>
<string name="delete_habits_message">"سيتم حذف عادات بشكل دائم. هذا العمل لا يمكن التراجع عنه."</string>
<string name="weekends">"عطلة نهاية الأسبوع"</string>
<!-- Fuzzy -->
<string name="any_weekday">"أيام الأسبوع"</string>
<!-- Fuzzy -->
<string name="any_day">"أي يوم"</string>
<!-- This is a bit unclear to me. It is like a prompt asking the user to select specific days, or is it more like an alarm mode where it will only activate on select days? -->
<string name="select_weekdays">"إختار أيام "</string>
<!-- Fuzzy -->
<string name="export_to_csv">"تصدير البيانات (CSV)"</string>
<string name="done_label">"منجز"</string>
<string name="clear_label">"نظف"</string>
<string name="select_hours">"تحديد ساعات"</string>
<string name="select_minutes">"تحديد دقائق "</string>
<!-- Short description used on the Google Play store. There is an 80-character limit. -->
<string name="store_short_description">"خلق عادات جيدة وتتبع تقدمك على مر الزمن"</string>
<string name="store_description_1">"لب يساعدك على خلق والحفاظ على العادات الجيدة، مما يسمح لك لتحقيق أهدافكة. الرسوم البيانية والإحصاءات التفصيلية تبين لكم كيف تحسن عاداتك مع مرور الوقت. هو تماما خالية من الاعلانات ومفتوحة المصدر."</string>
<string name="store_feature_interface">"&lt;b&gt;واجهة بسيطة، جميلة وحديثة &lt;/b&gt;
لوب يحتوي على واجهة بسيطة وهي سهلة الاستخدام و تتابع نظام تصميم الماتريل دسيجن."</string>
<string name="store_feature_score">"&lt;b&gt;نتيجة العادات&lt;/b&gt;
بالإضافة إلى عرض التقدم الحالي، لوب ديه خوارزمية متقدمة لحساب قوة عاداتك. كل التكرار يجعل هذه العادة أقوى، وفي كل يوم غاب يجعلها أضعف. مع ذلك غيب أيام قليلة بعد تقدم طويلة ، لن تدمر تماما تقدمك ."</string>
<string name="store_feature_statistics">"&lt;b&gt;الرسوم البيانية والإحصاءات المفصلة&lt;/b&gt;
نرى بوضوح كيف كنت قد تحسنت عاداتك بمرور الوقت مع الرسوم البيانية الجميله ومفصلة. انتقل إلى الوراء لنرى التاريخ الكامل لعاداتك."</string>
<string name="store_feature_schedules">"&lt;b&gt;جداول مرنة&lt;/b&gt;
تؤيد كل من العادات اليومية والعادات مع جداول أكثر تعقيدا، مثل 3 مرات كل أسبوع، مرة واحدة كل أسبوعين، أو مرة كل يومين."</string>
<string name="store_feature_reminders">"&lt;b&gt;تذكير&lt;/b&gt;
إنشاء تذكير لكل فرد من عاداتك، في ساعة اختيار من اليوم. تحقق بسهولة، رفض أو غفوة عادتك مباشرة من الإخطار، دون الحاجة إلى فتح التطبيق."</string>
<string name="store_feature_opensource">"&lt;b&gt;خالية تماما من الإعلانات و المصدر المفتوح&lt;/b&gt;
لا توجد على الاطلاق الإعلانات والشعارات المزعجة أو أذونات إضافية في هذا التطبيق، و سوف يكون هناك أبدا."</string>
<string name="store_feature_wear">"&lt;b&gt;الأمثل للساعات الذكية&lt;/b&gt;
يمكن التحقق من رسائل التذكير، رفض أو غفوة عادتك مباشرة من ساعتك الاندرويد وير. "</string>
<string name="about">"معلومات حول"</string>
<string name="translators">"المترجمين"</string>
<string name="developers">"المطورين"</string>
<!-- %s will get replaced by the version number. For example, "Versão %d" will become "Versão 1.2.0". -->
<string name="version_n">"الإصدار %s"</string>
<string name="frequency">"تردد"</string>
<string name="checkmark">"علامة الاختيار"</string>
<!-- This is a shorter version of "Habit Strength" -->
<string name="strength">"القوة"</string>
<string name="best_streaks">"أكثر تقدم"</string>
<string name="current_streaks">"تقدم الحالي"</string>
<string name="number_of_repetitions">"عدد من حالات التكرار"</string>
<string name="last_x_days">"آخر %d أيام"</string>
<string name="last_x_weeks">"آخر %d أسابيع"</string>
<string name="last_x_months">"آخر %d أشهر"</string>
<string name="last_x_years">"آخر %d سنين"</string>
<!-- "All time" number of repetitions. Or number of repetitions "since the beginning". -->
<string name="all_time">"كل الوقت"</string>
<string name="every_day">"كل يوم"</string>
<string name="every_week">"كل اسبوع"</string>
<string name="two_times_per_week">"مرتين في الأسبوع"</string>
<string name="five_times_per_week">"خمس مرات في الأسبوع"</string>
<string name="custom_frequency">"مخصص..."</string>
<string name="help">"مساعدة والأسئلة المتداولة"</string>
<string name="could_not_export">"فشل في تصدير البيانات."</string>
<string name="could_not_import">"فشل في استيراد البيانات."</string>
<!-- Appears when the user tries to import a file which we do not support or recognize. -->
<string name="file_not_recognized">"الملف غير المعترف."</string>
<string name="habits_imported">"نجح إستيراد العادات."</string>
<string name="full_backup_success">"نجح تصدير النسخ الاحتياطي الكامل."</string>
<string name="import_data">"استيراد بيانات."</string>
<string name="export_full_backup">"صدر نسخة احتياطية كاملة."</string>
<string name="import_data_summary">"تدعم النسخ الاحتياطي الكامل المصدرة من هذا التطبيق، فضلا عن الملفات التي تم إنشاؤها من Tickmate, HabitBull و Rewire. انظر التعليمات لمزيد من المعلومات."</string>
<string name="export_as_csv_summary">"صدر ملف التي يمكن فتحها ببرنامج جداول البيانات مثل إكسل أو وبينوفيس. لا يمكن إستيراد هذا الملف."</string>
<string name="export_full_backup_summary">"إنشاء ملف يحتوي على كافة البيانات. يمكن استيراد هذا الملف نفسه."</string>
<string name="bug_report_failed">"فشل في توليد تقرير الاعطال"</string>
<string name="generate_bug_report">"توليد تقرير الاعطال"</string>
<string name="troubleshooting">"استكشاف الأخطاء وإصلاحها"</string>
<string name="help_translate">"المساعدة في ترجمة هذا البرنامج"</string>
<string name="night_mode">"الوضع الليلي"</string>
<string name="use_pure_black">"استخدام أسود نقي في الوضع الليلي"</string>
<string name="pure_black_description">"يستبدل خلفيات رمادية مع أسود نقي في الوضع الليلي. يقلل من استهلاك البطارية في الهواتف مع شاشة AMOLED."</string>
<string name="interface_preferences">"السطح البيني"</string>
<string name="reverse_days">"ترتيب عكسي أيام"</string>
<string name="reverse_days_description">"عرض أيام في ترتيب عكسي على الشاشة الرئيسية"</string>
<string name="day">"يوم"</string>
<string name="week">"أسبوع"</string>
<string name="month">"شهر"</string>
<!-- Three-month period -->
<string name="quarter">"ربع سنه"</string>
<string name="year">"عام"</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">"مرات في"</string>
<string name="every_x_days">"كل %d أيام"</string>
<string name="every_x_weeks">"كل %d أسابيع"</string>
<string name="every_x_months">"كل %d أشهر"</string>
<!-- The old "habit strength" has been replaced by "score". Feel free to translate "score" as "strength" or "stability" if it sounds more natural in your language. -->
<string name="score">"النقاط"</string>
<string name="reminder_sound">"صوت تذكير"</string>
<!-- Appears when the user disables the reminder sound. Could also be "no sound", "mute" or "silent". -->
<string name="none">"صامت"</string>
</resources>
<string name="app_name">لوب ملاحق العادة </string>
<string name="main_activity_title">عادات</string>
<string name="action_settings">إعدادات</string>
<string name="edit">تعديل</string>
<string name="delete">حذف</string>
<string name="archive">أرشيف</string>
<string name="unarchive">إزالة من الأرشيف</string>
<string name="add_habit">إضافة العادة</string>
<string name="color_picker_default_title">غير اللون</string>
<string name="toast_habit_created">تم صنع عادة </string>
<string name="toast_habit_deleted">تم حذف عادة </string>
<string name="toast_habit_restored">تم ترجيع عادة</string>
<string name="toast_nothing_to_undo">لا شيء للتراجع</string>
<string name="toast_nothing_to_redo">لا شيء لتكرار</string>
<string name="toast_habit_changed">تم تغييرعادة</string>
<string name="toast_habit_changed_back">تم ترجيع العادة إلى أصلها</string>
<string name="toast_habit_archived">تم أرشيف العادات</string>
<string name="toast_habit_unarchived">تم إزالة العادة من الأرشيف </string>
<string name="overview">نظرة عامة</string>
<string name="habit_strength">قوة العادة</string>
<string name="history">التاريخ</string>
<string name="clear">مسح</string>
<string name="description_hint">السؤال (هل ... اليوم؟)</string>
<string name="repeat">كرر</string>
<string name="times_every">مرات في</string>
<string name="days">أيام</string>
<string name="reminder">تذكير</string>
<string name="discard">حذف</string>
<string name="save">حفظ</string>
<string name="streaks">تقدم متتالية</string>
<string name="no_habits_found"> لا يوجد لديك عادات مفعله</string>
<string name="long_press_to_toggle">أضغط و إستمر لتحقق أو ازل</string>
<string name="reminder_off">أوقف</string>
<string name="validation_name_should_not_be_blank">لا يمكن أن يكون الإسم فارغ</string>
<string name="validation_number_should_be_positive">يجب أن يكون الرقم إيجابي</string>
<string name="validation_at_most_one_rep_per_day">يمكن أن يكون التكرار واحدة فقط كل يوم </string>
<string name="create_habit">اخلق عادة</string>
<string name="edit_habit">تعديل العادة</string>
<string name="check">حقق</string>
<string name="snooze">لاحقا</string>
<!-- App introduction -->
<string name="intro_title_1">أهلا بك</string>
<string name="intro_description_1">لوب يساعدك على خلق والحفاظ على العادات الجيدة.</string>
<string name="intro_title_2">إنشاء بعض عادات جديدة</string>
<string name="intro_description_2">كل يوم، بعد أداء عادتك، وضع علامة على التطبيق.</string>
<string name="intro_title_3">حافظ على القيام بذلك</string>
<string name="intro_description_3">العادة المستمرة لفترات طويلة تكسب نجمة كامله</string>
<string name="intro_title_4">تتبع تقدمك</string>
<string name="intro_description_4">رسوم بيانية مفصلة تبين لكم كيف تحسن عاداتك مع مرور الوقت.</string>
<string name="interval_15_minutes">15 دقيقة</string>
<string name="interval_30_minutes">30 دقيقة</string>
<string name="interval_1_hour">ساعة واحدة</string>
<string name="interval_2_hour">ساعتين</string>
<string name="interval_4_hour">أربع ساعات</string>
<string name="interval_8_hour">ثماني ساعات</string>
<string name="interval_24_hour">24 ساعة</string>
<string name="pref_toggle_title">تبديل بكبسه</string>
<string name="pref_toggle_description">أكثر سهولة، لكنه ممكن يسبب كبسات غير مقصوده</string>
<string name="pref_snooze_interval_title">فترتي الغفوى على التذكير</string>
<string name="pref_rate_this_app">تقييم هذا التطبيق على جوجل بلاي</string>
<string name="pref_send_feedback">أرسل الملاحظات إلى المطور</string>
<string name="pref_view_source_code">إفحص التعليمات البرمجية على GitHub</string>
<string name="pref_view_app_introduction">عرض المقدمه</string>
<string name="links">روابط</string>
<string name="behavior">سلوك</string>
<string name="name">اسم</string>
<string name="settings">إعدادات</string>
<string name="snooze_interval">فترتي الغفوه</string>
<string name="hint_title">هل كنت تعلم؟</string>
<string name="hint_drag">لإعادة ترتيب القوائم، أضغط اسم من هذه العادة، ثم اسحبه إلى المكان الصحيح.</string>
<string name="hint_landscape">يمكنك ان ترى المزيد أيام عن طريق وضع الهاتف في وضع أفقي.</string>
<string name="delete_habits">حذف عادات</string>
<string name="delete_habits_message">سيتم حذف عادات بشكل دائم. هذا العمل لا يمكن التراجع عنه.</string>
<string name="weekends">عطلة نهاية الأسبوع</string>
<string name="any_weekday">أيام الأسبوع</string>
<string name="any_day">أي يوم</string>
<string name="select_weekdays">إختار أيام </string>
<string name="export_to_csv">تصدير البيانات (CSV)</string>
<string name="done_label">منجز</string>
<string name="clear_label">نظف</string>
<string name="select_hours">تحديد ساعات</string>
<string name="select_minutes">تحديد دقائق </string>
<string name="about">معلومات حول</string>
<string name="translators">المترجمين</string>
<string name="developers">المطورين</string>
<string name="version_n">الإصدار %s</string>
<string name="frequency">تردد</string>
<string name="checkmark">علامة الاختيار</string>
<string name="strength">القوة</string>
<string name="best_streaks">أكثر تقدم</string>
<string name="current_streaks">تقدم الحالي</string>
<string name="number_of_repetitions">عدد من حالات التكرار</string>
<string name="last_x_days">آخر %d أيام</string>
<string name="last_x_weeks">آخر %d أسابيع</string>
<string name="last_x_months">آخر %d أشهر</string>
<string name="last_x_years">آخر %d سنين</string>
<string name="all_time">كل الوقت</string>
<string name="every_day">كل يوم</string>
<string name="every_week">كل اسبوع</string>
<string name="two_times_per_week">مرتين في الأسبوع</string>
<string name="five_times_per_week">خمس مرات في الأسبوع</string>
<string name="custom_frequency">مخصص...</string>
<string name="help">مساعدة والأسئلة المتداولة</string>
<string name="could_not_export">فشل في تصدير البيانات.</string>
<string name="could_not_import">فشل في استيراد البيانات.</string>
<string name="file_not_recognized">الملف غير المعترف.</string>
<string name="habits_imported">نجح إستيراد العادات.</string>
<string name="full_backup_success">نجح تصدير النسخ الاحتياطي الكامل.</string>
<string name="import_data">استيراد بيانات.</string>
<string name="export_full_backup">صدر نسخة احتياطية كاملة.</string>
<string name="import_data_summary">تدعم النسخ الاحتياطي الكامل المصدرة من هذا التطبيق، فضلا عن الملفات التي تم إنشاؤها من Tickmate, HabitBull و Rewire. انظر التعليمات لمزيد من المعلومات.</string>
<string name="export_as_csv_summary">صدر ملف التي يمكن فتحها ببرنامج جداول البيانات مثل إكسل أو وبينوفيس. لا يمكن إستيراد هذا الملف.</string>
<string name="export_full_backup_summary">إنشاء ملف يحتوي على كافة البيانات. يمكن استيراد هذا الملف نفسه.</string>
<string name="bug_report_failed">فشل في توليد تقرير الاعطال</string>
<string name="generate_bug_report">توليد تقرير الاعطال</string>
<string name="troubleshooting">استكشاف الأخطاء وإصلاحها</string>
<string name="help_translate">المساعدة في ترجمة هذا البرنامج</string>
<string name="night_mode">الوضع الليلي</string>
<string name="use_pure_black">استخدام أسود نقي في الوضع الليلي</string>
<string name="pure_black_description">يستبدل خلفيات رمادية مع أسود نقي في الوضع الليلي. يقلل من استهلاك البطارية في الهواتف مع شاشة AMOLED.</string>
<string name="interface_preferences">السطح البيني</string>
<string name="reverse_days">ترتيب عكسي أيام</string>
<string name="reverse_days_description">عرض أيام في ترتيب عكسي على الشاشة الرئيسية</string>
<string name="day">يوم</string>
<string name="week">أسبوع</string>
<string name="month">شهر</string>
<string name="quarter">ربع سنه</string>
<string name="year">عام</string>
<string name="total">المجموع</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">مرات في</string>
<string name="every_x_days">كل %d أيام</string>
<string name="every_x_weeks">كل %d أسابيع</string>
<string name="every_x_months">كل %d أشهر</string>
<string name="score">النقاط</string>
<string name="reminder_sound">صوت تذكير</string>
<string name="none">صامت</string>
<string name="action">عمل</string>
<string name="download">تحميل</string>
<string name="export">استخراج</string>
</resources>

View File

@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Loop Следене на навици</string>
<string name="main_activity_title">Навици</string>
<string name="action_settings">Настройки</string>
<string name="edit">Редактиране</string>
<string name="delete">Изтриване</string>
<string name="archive">Архивиране</string>
<string name="unarchive">Разархивиране</string>
<string name="add_habit">Добавяне на навик</string>
<string name="color_picker_default_title">Промяна на цвят</string>
<string name="toast_habit_created">Навикът е създаден</string>
<string name="toast_habit_deleted">Навиците са изтрити</string>
<string name="toast_habit_restored">Навиците са възстановени</string>
<string name="toast_nothing_to_undo">Нищо за отмяна</string>
<string name="toast_nothing_to_redo">Нищо за възстановяване</string>
<string name="toast_habit_changed">Навикът е променен</string>
<string name="toast_habit_changed_back">Промяната на навика е отменена.</string>
<string name="toast_habit_archived">Навиците са архивирани</string>
<string name="toast_habit_unarchived">Навиците са разархивирани</string>
<string name="overview">Обобщение</string>
<string name="habit_strength">Сила на навика</string>
<string name="history">История</string>
<string name="clear">Изчистване</string>
<string name="description_hint">Въпрос (Днес, ... ли?)</string>
<string name="repeat">Повтори</string>
<string name="times_every">пъти в период от</string>
<string name="days">дни</string>
<string name="reminder">Напомняне</string>
<string name="discard">Отказ</string>
<string name="save">Запазване</string>
<string name="streaks">Поредици</string>
<string name="no_habits_found">Нямате активни навици</string>
<string name="long_press_to_toggle">Натиснете и задръжте за да добавите или премахнете отметка</string>
<string name="reminder_off">Изключено</string>
<string name="validation_name_should_not_be_blank">Името не може да бъде празно.</string>
<string name="validation_number_should_be_positive">Числото трябва да е положително.</string>
<string name="validation_at_most_one_rep_per_day">Позволено е до едно повторение на ден.</string>
<string name="create_habit">Създаване на навик</string>
<string name="edit_habit">Редактиране на навик</string>
<string name="check">Поставяне на отметка</string>
<string name="snooze">По-късно</string>
<!-- App introduction -->
<string name="intro_title_1">Добре дошли</string>
<string name="intro_description_1">Loop Следене на навици ви помага да създавате и поддържате добри навици.</string>
<string name="intro_title_2">Създайте нови навици</string>
<string name="intro_description_2">Всеки ден, след изпълнението на навика, поставете отметка в приложението.</string>
<string name="intro_title_3">Продължавайте да го изпълнявате</string>
<string name="intro_description_3">Навици изпълнявани редовно за дълго време ще получат пълна звезда.</string>
<string name="intro_title_4">Следете прогреса си</string>
<string name="intro_description_4">Подробни диаграми ви показват как вашите навици са се подобрили с времето.</string>
<string name="interval_15_minutes">15 минути</string>
<string name="interval_30_minutes">30 минути</string>
<string name="interval_1_hour">1 час</string>
<string name="interval_2_hour">2 часа</string>
<string name="interval_4_hour">4 часа</string>
<string name="interval_8_hour">8 часа</string>
<string name="interval_24_hour">24 часа</string>
<string name="pref_toggle_title">Маркиране с кратко натискане</string>
<string name="pref_toggle_description">Поставяне на отметки с кратко натискане вместо с натискане и задържане. По-удобно, но може да доведе до неволно маркиране.</string>
<string name="pref_snooze_interval_title">Интервал на напомняне след отлагане</string>
<string name="pref_rate_this_app">Оценяване на това приложение в Google Play</string>
<string name="pref_send_feedback">Изпращане на отзиви към разработчика</string>
<string name="pref_view_source_code">Преглед на програмния код в GitHub</string>
<string name="pref_view_app_introduction">Преглед на въведение в приложението</string>
<string name="links">Препратки</string>
<string name="behavior">Поведение</string>
<string name="name">Име</string>
<string name="settings">Настройки</string>
<string name="snooze_interval">Интервал на отлагане</string>
<string name="hint_title">Знаете ли че?</string>
<string name="hint_drag">За да пренаредите записите, натиснете и задръжте върху името на навика и го придърпайте до правилното място.</string>
<string name="hint_landscape">Може да виждате повече дни като обърнете телефона си в хоризонтално положение.</string>
<string name="delete_habits">Изтриване на навици</string>
<string name="delete_habits_message">Навиците ще се изтрият перманентно. Това действие не може да бъде отменено.</string>
<string name="habit_not_found">Навикът е изтрит / не е намерен</string>
<string name="weekends">Събота и неделя</string>
<string name="any_weekday">От понеделник до петък</string>
<string name="any_day">Всеки ден от седмицата</string>
<string name="select_weekdays">Избор на дни</string>
<string name="export_to_csv">Експортиране като CSV</string>
<string name="done_label">Готово</string>
<string name="clear_label">Изчистване</string>
<string name="select_hours">Избиране на час</string>
<string name="select_minutes">Избиране на минута</string>
<string name="about">За приложението</string>
<string name="translators">Преводачи</string>
<string name="developers">Разработчици</string>
<string name="version_n">Версия %s</string>
<string name="frequency">Честота</string>
<string name="checkmark">Отметка</string>
<string name="strength">Сила</string>
<string name="best_streaks">Най-добри поредици</string>
<string name="current_streaks">Текуща поредица</string>
<string name="number_of_repetitions">Брой повторения</string>
<string name="last_x_days">Последните %d дни</string>
<string name="last_x_weeks">Последните %d седмици</string>
<string name="last_x_months">Последните %d месеци</string>
<string name="last_x_years">Последните %d години</string>
<string name="all_time">От началото</string>
<string name="every_day">Всеки ден</string>
<string name="every_week">Всяка седмица</string>
<string name="two_times_per_week">2 пъти седмично</string>
<string name="five_times_per_week">5 пъти седмично</string>
<string name="custom_frequency">Друго ...</string>
<string name="help">Помощ &amp; ЧЗВ</string>
<string name="could_not_export">Неуспешно експортиране на данни.</string>
<string name="could_not_import">Неуспешно импортиране на данни.</string>
<string name="file_not_recognized">Файлът не е разпознат.</string>
<string name="habits_imported">Навиците са импортирани успешно.</string>
<string name="full_backup_success">Пълно резервно копие е експортирано успешно.</string>
<string name="import_data">Импортиране на данни</string>
<string name="export_full_backup">Експортиране на пълно резервно копие</string>
<string name="import_data_summary">Поддържа пълни резервни копия експортирани чрез това приложение, както и файлове генерирани чрез Tickmate, HabitBull или Rewire. Вижте ЧЗВ за повече информация.</string>
<string name="export_as_csv_summary">Генерира файлове, които могат да се отварят със софтуер за електронни таблици като Microsoft Excel или OpenOffice Calc. Този файл не може да се импортира обратно.</string>
<string name="export_full_backup_summary">Генерира файл, който съдържа всичките ви данни. Този файл може да бъде импортиран обратно.</string>
<string name="bug_report_failed">Неуспешно генериране на доклад за грешки.</string>
<string name="generate_bug_report">Генериране на доклад за грешки</string>
<string name="troubleshooting">Отстраняване на проблеми</string>
<string name="help_translate">Помагане за превода на това приложение</string>
<string name="night_mode">Нощен режим</string>
<string name="use_pure_black">Използване на чисто черно при нощен режим</string>
<string name="pure_black_description">Заменя сивите фонове с чисто черни при нощен режим. Намаля разхода на батерията при телефони с AMOLED дисплеи.</string>
<string name="interface_preferences">Интерфейс</string>
<string name="reverse_days">Обратен ред на дните</string>
<string name="reverse_days_description">Показва дните на основния екран в обратен ред</string>
<string name="day">Ден</string>
<string name="week">Седмица</string>
<string name="month">Месец</string>
<string name="quarter">Тримесечие</string>
<string name="year">Година</string>
<string name="total">Общо</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">път в период от</string>
<string name="every_x_days">На всеки %d дни</string>
<string name="every_x_weeks">На всеки %d седмици</string>
<string name="every_x_months">На всеки %d месеца</string>
<string name="score">Сила</string>
<string name="reminder_sound">Звук за напомняне</string>
<string name="none">Няма</string>
<string name="filter">Филтър</string>
<string name="hide_completed">Скриване на завършените</string>
<string name="hide_archived">Скриване на архивираните</string>
<string name="sticky_notifications">Направи нотификациите постоянни</string>
<string name="sticky_notifications_description">Предотвратява изчистването на нотификацията с плъзване настрани.</string>
<string name="repair_database">Поправка на базата данни</string>
<string name="database_repaired">Базата данни е поправена.</string>
<string name="uncheck">Премахване на отметка</string>
<string name="toggle">Смяна</string>
<string name="action">Действие</string>
<string name="habit">Навик</string>
<string name="sort">Сортиране</string>
<string name="manually">Ръчно</string>
<string name="by_name">По име</string>
<string name="by_color">По цвят</string>
<string name="by_score">По сила</string>
<string name="download">Изтегляне</string>
<string name="export">Експортиране</string>
</resources>

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