mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 957a5b7c17 | |||
| 64cc9f78a8 | |||
| e50c411d1e | |||
| ce27773138 | |||
| 0864f83307 | |||
| 624cc67d9b | |||
| 462bac8167 | |||
| 5865eb41f7 | |||
| 2bfd4a942d | |||
| b4a33cba39 | |||
| f02c86e61b | |||
| 5021f50e18 | |||
| bf3964a231 | |||
| 657cde75d8 | |||
| c56b86d32c | |||
| 16f20d50a0 | |||
| 7bb88dcb97 | |||
| 7bb62c197f | |||
| 540a618ba8 | |||
| bf24cc608c | |||
| a73459784e | |||
| de3b97dfdf | |||
| 91996924d9 | |||
| 9fe446b424 | |||
| 3857eaf5e9 | |||
| e29fb58922 | |||
| 404fc869b0 | |||
| 001dd5a7c1 | |||
| 7930cc8f31 | |||
| 526830ba61 | |||
| fe1513bb64 | |||
| e06ace9ea8 | |||
| d727dabb2b | |||
| d17e8fcbfb | |||
| be3d7145ab | |||
| cf66587644 | |||
| 0dc9ec2e5f | |||
| 0a375ded96 | |||
| fa5d6f8fee | |||
| 534e6c2d9d | |||
| b6501c9a29 | |||
| 238a1c724d | |||
| 34ca9d17a2 | |||
| e844390614 | |||
| 5e00d07b73 | |||
| 28b6ae7014 | |||
| 2a1bf5fc2e | |||
| ef7483f9dc | |||
| bbb9ed8f99 | |||
| c49d576871 | |||
| bc66ae4f7a | |||
| fa416adbb9 | |||
| 8b835b9918 | |||
| 471c5d341f | |||
| 57296745b3 | |||
| 140ab34a76 | |||
| 0d6ad26505 | |||
| 65cc99dbf7 | |||
| 6855ef9d5e | |||
| 4c58b084c6 | |||
| 5c8e522646 | |||
| 55da0759d4 | |||
| 692fe3218f | |||
| 387930c08d | |||
| 6bd31f9607 | |||
| 9aafe7160c | |||
| 5cc4aac67a | |||
| 831421bc98 | |||
| 161d8f2517 | |||
| bfe4b822b3 | |||
| 19e79a8559 | |||
| 876d4f0ac7 | |||
| f4f7faf3a4 | |||
| 56f2ae57fe | |||
| 3fe09efe9b | |||
| f0de29fbfe | |||
| 324facfffd | |||
| 03e58f9ef2 | |||
| e6c9f7f0c9 | |||
| 42bdedb86a | |||
| ab0c510fda | |||
| e46fd58664 | |||
| 8532bd402e | |||
| 2c599b18ef | |||
| 0d78ba4ba9 | |||
|
|
611dfa00a5 | ||
|
|
54a195243d | ||
|
|
4fc30fae53 | ||
| b3fe9c65d2 | |||
| 09f1ae8765 | |||
| 0a8b763ece | |||
| edd5f25529 | |||
| d81fdb41dc | |||
| 02c8810e46 | |||
| 6adf8061d3 | |||
| d19d57e5df | |||
| fd82e6c24b | |||
| 56263efa39 | |||
| d5eacba303 | |||
| 222261c674 | |||
| b1a06df7f8 | |||
| a1fc7dd0d1 | |||
|
|
10131d5124 | ||
| aa94959ad2 | |||
| 967dc2586b | |||
| 908fd1d6ae | |||
| 45fd8a29e1 | |||
| 4624acd477 | |||
| c8cd4fa389 | |||
| 8c4fab28aa |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ captures/
|
||||
docs/
|
||||
gen/
|
||||
local.properties
|
||||
crowdin.yaml
|
||||
local
|
||||
secret/
|
||||
|
||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
### 1.7.11 (Aug 10, 2019)
|
||||
|
||||
* Fix bug that produced corrupted CSV files in some countries
|
||||
|
||||
### 1.7.10 (June 15, 2019)
|
||||
|
||||
* Fix bug that prevented some devices from showing notifications.
|
||||
* Update targetSdk to Android Pie (API level 28)
|
||||
|
||||
### 1.7.8 (April 21, 2018)
|
||||
|
||||
* Add support for adaptive icons (Oreo)
|
||||
* Add support for notification channels (Oreo)
|
||||
* Update translations
|
||||
|
||||
### 1.7.7 (September 30, 2017)
|
||||
|
||||
* Fix bug that caused reminders to show repeatedly on DST changes
|
||||
|
||||
### 1.7.6 (July 18, 2017)
|
||||
|
||||
* Fix bug that caused widgets not to render sometimes
|
||||
* Fix other minor bugs
|
||||
* Update translations
|
||||
|
||||
### 1.7.3 (May 30, 2017)
|
||||
|
||||
* Improve performance of 'sort by score'
|
||||
* Other minor bug fixes
|
||||
|
||||
### 1.7.2 (May 27, 2017)
|
||||
|
||||
* Fix crash at startup
|
||||
|
||||
### 1.7.1 (May 21, 2017)
|
||||
|
||||
* Fix crash (BadParcelableException)
|
||||
* Fix layout for RTL languages such as Arabic
|
||||
* Automatically detect and reject invalid database files
|
||||
* Add Hebrew translation
|
||||
|
||||
### 1.7.0 (Mar 31, 2017)
|
||||
|
||||
* Sort habits automatically
|
||||
* Allow swiping the header to see previous days
|
||||
* Import backups directly from Google Drive or Dropbox
|
||||
* Refresh data automatically at midnight
|
||||
* Other minor bug fixes and enhancements
|
||||
|
||||
### 1.6.2 (Oct 13, 2016)
|
||||
|
||||
* Fix crash on Android 4.1
|
||||
|
||||
### 1.6.1 (Oct 10, 2016)
|
||||
|
||||
* Fix a crash at startup when database is corrupted
|
||||
|
||||
### 1.6.0 (Oct 10, 2016)
|
||||
|
||||
* Add option to make notifications sticky
|
||||
|
||||
@@ -2,30 +2,50 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
apply plugin: 'me.tatarka.retrolambda'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
|
||||
// signingConfigs {
|
||||
// release {
|
||||
// storeFile file(LOOP_STORE_FILE)
|
||||
// storePassword LOOP_STORE_PASSWORD
|
||||
// keyAlias LOOP_KEY_ALIAS
|
||||
// keyPassword LOOP_KEY_PASSWORD
|
||||
// }
|
||||
// }
|
||||
|
||||
playAccountConfigs {
|
||||
defaultAccountConfig {
|
||||
jsonFile = file('../secret/playstore.json')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.isoron.uhabits"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
targetSdkVersion 28
|
||||
|
||||
buildConfigField "Integer", "databaseVersion", "14"
|
||||
buildConfigField "Integer", "databaseVersion", "15"
|
||||
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArgument "size", "medium"
|
||||
|
||||
// playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
// signingConfig signingConfigs.release
|
||||
}
|
||||
debug {
|
||||
testCoverageEnabled = true
|
||||
testCoverageEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +73,7 @@ dependencies {
|
||||
|
||||
androidTestApt 'com.google.dagger:dagger-compiler:2.2'
|
||||
|
||||
androidTestCompile 'com.android.support:support-annotations:23.3.0'
|
||||
androidTestCompile 'com.android.support:support-annotations:27.1.1'
|
||||
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,16 +84,15 @@ 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:27.1.1'
|
||||
compile 'com.android.support:design:27.1.1'
|
||||
compile 'com.android.support:preference-v14:27.1.1'
|
||||
compile 'com.android.support:support-v4:27.1.1'
|
||||
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'
|
||||
compile 'com.google.dagger:dagger:2.2'
|
||||
compile 'com.jakewharton:butterknife:8.0.1'
|
||||
compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
|
||||
compile 'com.opencsv:opencsv:3.7'
|
||||
compile 'org.apmem.tools:layouts:1.10@aar'
|
||||
compile 'org.jetbrains:annotations-java5:15.0'
|
||||
@@ -140,3 +159,7 @@ task coverageReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
|
||||
classDirectories = files(fileTree(dir: classDir, excludes: excludes))
|
||||
executionData = files(jvmExecData, connectedExecData)
|
||||
}
|
||||
|
||||
//play {
|
||||
// track = 'alpha'
|
||||
//}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.test.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
@@ -31,6 +32,7 @@ import org.isoron.uhabits.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@@ -63,6 +65,10 @@ public class BaseAndroidTest
|
||||
|
||||
protected AndroidTestComponent component;
|
||||
|
||||
protected ModelFactory modelFactory;
|
||||
|
||||
private boolean isDone = false;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
@@ -89,7 +95,7 @@ public class BaseAndroidTest
|
||||
taskRunner = component.getTaskRunner();
|
||||
logger = component.getHabitsLogger();
|
||||
|
||||
ModelFactory modelFactory = component.getModelFactory();
|
||||
modelFactory = component.getModelFactory();
|
||||
fixtures = new HabitFixtures(modelFactory, habitList);
|
||||
|
||||
latch = new CountDownLatch(1);
|
||||
@@ -113,6 +119,25 @@ public class BaseAndroidTest
|
||||
assertTrue(latch.await(60, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
protected void runConcurrently(Runnable... runnableList) throws Exception
|
||||
{
|
||||
isDone = false;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
||||
List<Future> futures = new LinkedList<>();
|
||||
for (Runnable r : runnableList)
|
||||
futures.add(executor.submit(() ->
|
||||
{
|
||||
while (!isDone) r.run();
|
||||
return null;
|
||||
}));
|
||||
|
||||
Thread.sleep(3000);
|
||||
isDone = true;
|
||||
executor.shutdown();
|
||||
for(Future f : futures) f.get();
|
||||
while (!executor.isTerminated()) Thread.sleep(50);
|
||||
}
|
||||
|
||||
protected void setTheme(@StyleRes int themeId)
|
||||
{
|
||||
targetContext.setTheme(themeId);
|
||||
@@ -130,4 +155,18 @@ public class BaseAndroidTest
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
protected void startTracing()
|
||||
{
|
||||
File dir = FileUtils.getFilesDir(targetContext, "Profile");
|
||||
assertNotNull(dir);
|
||||
String tracePath = dir.getAbsolutePath() + "/performance.trace";
|
||||
Log.d("PerformanceTest", String.format("Saving trace file to %s", tracePath));
|
||||
Debug.startMethodTracingSampling(tracePath, 0, 1000);
|
||||
}
|
||||
|
||||
protected void stopTracing()
|
||||
{
|
||||
Debug.stopMethodTracing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ public class BaseViewTest extends BaseAndroidTest
|
||||
throws IOException
|
||||
{
|
||||
File dir = FileUtils.getSDCardDir("test-screenshots");
|
||||
if (dir == null) dir = FileUtils.getFilesDir("test-screenshots");
|
||||
if (dir == null) dir = FileUtils.getFilesDir(targetContext,"test-screenshots");
|
||||
if (dir == null) throw new RuntimeException(
|
||||
"Could not find suitable dir for screenshots");
|
||||
|
||||
|
||||
@@ -39,12 +39,18 @@ public class HabitFixtures
|
||||
}
|
||||
|
||||
public Habit createEmptyHabit()
|
||||
{
|
||||
return createEmptyHabit(null);
|
||||
}
|
||||
|
||||
public Habit createEmptyHabit(Long id)
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setName("Meditate");
|
||||
habit.setDescription("Did you meditate this morning?");
|
||||
habit.setColor(3);
|
||||
habit.setFrequency(Frequency.DAILY);
|
||||
habit.setId(id);
|
||||
habitList.add(habit);
|
||||
return habit;
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ public class CheckmarkButtonViewTest extends BaseViewTest
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
setSimilarityCutoff(0.03f);
|
||||
setSimilarityCutoff(0.015f);
|
||||
|
||||
latch = new CountDownLatch(1);
|
||||
view = new CheckmarkButtonView(targetContext);
|
||||
view.setValue(Checkmark.UNCHECKED);
|
||||
view.setColor(ColorUtils.getAndroidTestColor(7));
|
||||
view.setColor(ColorUtils.getAndroidTestColor(5));
|
||||
|
||||
measureView(view, dpToPixels(40), dpToPixels(40));
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest
|
||||
view = new CheckmarkPanelView(targetContext);
|
||||
view.setHabit(habit);
|
||||
view.setCheckmarkValues(checkmarks);
|
||||
view.setButtonCount(4);
|
||||
view.setColor(ColorUtils.getAndroidTestColor(7));
|
||||
|
||||
measureView(view, dpToPixels(200), dpToPixels(200));
|
||||
|
||||
@@ -171,13 +171,13 @@ public class MainTest
|
||||
clickMenuItem(R.string.archive);
|
||||
assertHabitsDontExist(names);
|
||||
|
||||
clickMenuItem(R.string.show_archived);
|
||||
clickMenuItem(R.string.hide_archived);
|
||||
|
||||
assertHabitsExist(names);
|
||||
selectHabits(names);
|
||||
clickMenuItem(R.string.unarchive);
|
||||
|
||||
clickMenuItem(R.string.show_archived);
|
||||
clickMenuItem(R.string.hide_archived);
|
||||
|
||||
assertHabitsExist(names);
|
||||
deleteHabits(names);
|
||||
|
||||
@@ -40,8 +40,6 @@ import static org.junit.Assert.*;
|
||||
@MediumTest
|
||||
public class ImportTest extends BaseAndroidTest
|
||||
{
|
||||
private File baseDir;
|
||||
|
||||
private Context context;
|
||||
|
||||
@Override
|
||||
@@ -50,11 +48,8 @@ public class ImportTest extends BaseAndroidTest
|
||||
{
|
||||
super.setUp();
|
||||
DateUtils.setFixedLocalTime(null);
|
||||
|
||||
fixtures.purgeHabits(habitList);
|
||||
context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
baseDir = FileUtils.getFilesDir("Backups");
|
||||
if (baseDir == null) fail("baseDir should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -149,8 +144,7 @@ public class ImportTest extends BaseAndroidTest
|
||||
|
||||
private void importFromFile(String assetFilename) throws IOException
|
||||
{
|
||||
File file =
|
||||
new File(String.format("%s/%s", baseDir.getPath(), assetFilename));
|
||||
File file = File.createTempFile("asset", "");
|
||||
copyAssetToFile(assetFilename, file);
|
||||
assertTrue(file.exists());
|
||||
assertTrue(file.canRead());
|
||||
@@ -159,5 +153,7 @@ public class ImportTest extends BaseAndroidTest
|
||||
assertThat(importer.canHandle(file), is(true));
|
||||
|
||||
importer.importHabitsFromFile(file);
|
||||
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.models;
|
||||
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import org.hamcrest.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import static junit.framework.Assert.*;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.isoron.uhabits.models.HabitList.Order.*;
|
||||
|
||||
@SuppressWarnings("JavaDoc")
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@MediumTest
|
||||
public class HabitListTest extends BaseAndroidTest
|
||||
{
|
||||
private ArrayList<Habit> habitsArray;
|
||||
|
||||
private HabitList activeHabits;
|
||||
|
||||
private HabitList reminderHabits;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
habitList.removeAll();
|
||||
|
||||
habitsArray = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Habit habit = fixtures.createEmptyHabit((long) i);
|
||||
habitsArray.add(habit);
|
||||
|
||||
if (i % 3 == 0)
|
||||
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
|
||||
|
||||
habitList.update(habit);
|
||||
}
|
||||
|
||||
habitsArray.get(0).setArchived(true);
|
||||
habitsArray.get(1).setArchived(true);
|
||||
habitsArray.get(4).setArchived(true);
|
||||
habitsArray.get(7).setArchived(true);
|
||||
|
||||
activeHabits = habitList.getFiltered(new HabitMatcherBuilder().build());
|
||||
|
||||
reminderHabits = habitList.getFiltered(new HabitMatcherBuilder()
|
||||
.setArchivedAllowed(true)
|
||||
.setReminderRequired(true)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_size()
|
||||
{
|
||||
assertThat(habitList.size(), equalTo(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_countActive()
|
||||
{
|
||||
assertThat(activeHabits.size(), equalTo(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getByPosition()
|
||||
{
|
||||
assertThat(habitList.getByPosition(0), equalTo(habitsArray.get(0)));
|
||||
assertThat(habitList.getByPosition(3), equalTo(habitsArray.get(3)));
|
||||
assertThat(habitList.getByPosition(9), equalTo(habitsArray.get(9)));
|
||||
|
||||
assertThat(activeHabits.getByPosition(0), equalTo(habitsArray.get(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getHabitsWithReminder()
|
||||
{
|
||||
assertThat(reminderHabits.size(), equalTo(4));
|
||||
assertThat(reminderHabits.getByPosition(1),
|
||||
equalTo(habitsArray.get(3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_get_withInvalidId()
|
||||
{
|
||||
assertThat(habitList.getById(100L), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_get_withValidId()
|
||||
{
|
||||
Habit habit1 = habitsArray.get(0);
|
||||
Habit habit2 = habitList.getById(habit1.getId());
|
||||
assertThat(habit1, equalTo(habit2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_reorder()
|
||||
{
|
||||
int operations[][] = {
|
||||
{ 5, 2 }, { 3, 7 }, { 4, 4 }, { 3, 2 }
|
||||
};
|
||||
|
||||
int expectedPosition[][] = {
|
||||
{ 0, 1, 3, 4, 5, 2, 6, 7, 8, 9 },
|
||||
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
|
||||
{ 0, 1, 7, 3, 4, 2, 5, 6, 8, 9 },
|
||||
{ 0, 1, 7, 2, 4, 3, 5, 6, 8, 9 },
|
||||
};
|
||||
|
||||
for (int i = 0; i < operations.length; i++)
|
||||
{
|
||||
int from = operations[i][0];
|
||||
int to = operations[i][1];
|
||||
|
||||
Habit fromHabit = habitList.getByPosition(from);
|
||||
Habit toHabit = habitList.getByPosition(to);
|
||||
habitList.reorder(fromHabit, toHabit);
|
||||
|
||||
int actualPositions[] = new int[10];
|
||||
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
Habit h = habitList.getById(j);
|
||||
assertNotNull(h);
|
||||
actualPositions[j] = habitList.indexOf(h);
|
||||
}
|
||||
|
||||
assertThat(actualPositions, equalTo(expectedPosition[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_writeCSV() throws IOException
|
||||
{
|
||||
habitList.removeAll();
|
||||
|
||||
Habit h1 = fixtures.createEmptyHabit();
|
||||
h1.setName("Meditate");
|
||||
h1.setDescription("Did you meditate this morning?");
|
||||
h1.setFrequency(Frequency.DAILY);
|
||||
h1.setColor(3);
|
||||
|
||||
Habit h2 = fixtures.createEmptyHabit();
|
||||
h2.setName("Wake up early");
|
||||
h2.setDescription("Did you wake up before 6am?");
|
||||
h2.setFrequency(new Frequency(2, 3));
|
||||
h2.setColor(5);
|
||||
|
||||
habitList.update(h1);
|
||||
habitList.update(h2);
|
||||
|
||||
String expectedCSV =
|
||||
"Position,Name,Description,NumRepetitions,Interval,Color\n" +
|
||||
"001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
|
||||
"002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
habitList.writeCSV(writer);
|
||||
|
||||
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_ordering()
|
||||
{
|
||||
habitList.removeAll();
|
||||
|
||||
Habit h3 = fixtures.createEmptyHabit();
|
||||
h3.setName("C Habit");
|
||||
h3.setColor(0);
|
||||
habitList.update(h3);
|
||||
|
||||
Habit h1 = fixtures.createEmptyHabit();
|
||||
h1.setName("A Habit");
|
||||
h1.setColor(2);
|
||||
habitList.update(h1);
|
||||
|
||||
Habit h4 = fixtures.createEmptyHabit();
|
||||
h4.setName("D Habit");
|
||||
h4.setColor(1);
|
||||
habitList.update(h4);
|
||||
|
||||
Habit h2 = fixtures.createEmptyHabit();
|
||||
h2.setName("B Habit");
|
||||
h2.setColor(2);
|
||||
habitList.update(h2);
|
||||
|
||||
habitList.setOrder(BY_POSITION);
|
||||
assertThat(habitList.getByPosition(0), equalTo(h3));
|
||||
assertThat(habitList.getByPosition(1), equalTo(h1));
|
||||
assertThat(habitList.getByPosition(2), equalTo(h4));
|
||||
assertThat(habitList.getByPosition(3), equalTo(h2));
|
||||
|
||||
habitList.setOrder(BY_NAME);
|
||||
assertThat(habitList.getByPosition(0), equalTo(h1));
|
||||
assertThat(habitList.getByPosition(1), equalTo(h2));
|
||||
assertThat(habitList.getByPosition(2), equalTo(h3));
|
||||
assertThat(habitList.getByPosition(3), equalTo(h4));
|
||||
|
||||
habitList.remove(h1);
|
||||
habitList.add(h1);
|
||||
assertThat(habitList.getByPosition(0), equalTo(h1));
|
||||
|
||||
habitList.setOrder(BY_COLOR);
|
||||
assertThat(habitList.getByPosition(0), equalTo(h3));
|
||||
assertThat(habitList.getByPosition(1), equalTo(h4));
|
||||
assertThat(habitList.getByPosition(2), equalTo(h1));
|
||||
assertThat(habitList.getByPosition(3), equalTo(h2));
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,41 @@ public class SQLiteCheckmarkListTest extends BaseAndroidTest
|
||||
assertThat(records.get(0).timestamp, equalTo(today - 21 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFixRecords() throws Exception
|
||||
{
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
long from = DateUtils.getStartOfToday();
|
||||
long to = from + 5 * day;
|
||||
|
||||
List<CheckmarkRecord> original, actual, expected;
|
||||
HabitRecord habit = new HabitRecord();
|
||||
|
||||
original = new ArrayList<>();
|
||||
original.add(new CheckmarkRecord(habit, from + 8*day, 2));
|
||||
original.add(new CheckmarkRecord(habit, from + 5*day, 0));
|
||||
original.add(new CheckmarkRecord(habit, from + 4*day, 0));
|
||||
original.add(new CheckmarkRecord(habit, from + 4*day, 2));
|
||||
original.add(new CheckmarkRecord(habit, from + 3*day, 2));
|
||||
original.add(new CheckmarkRecord(habit, from + 2*day, 1));
|
||||
original.add(new CheckmarkRecord(habit, from + 2*day + 100, 1));
|
||||
original.add(new CheckmarkRecord(habit, from, 0));
|
||||
original.add(new CheckmarkRecord(habit, from, 2));
|
||||
original.add(new CheckmarkRecord(habit, from - day, 2));
|
||||
|
||||
actual = SQLiteCheckmarkList.fixRecords(original, habit, from, to);
|
||||
|
||||
expected = new ArrayList<>();
|
||||
expected.add(new CheckmarkRecord(habit, from + 5*day, 0));
|
||||
expected.add(new CheckmarkRecord(habit, from + 4*day, 2));
|
||||
expected.add(new CheckmarkRecord(habit, from + 3*day, 2));
|
||||
expected.add(new CheckmarkRecord(habit, from + 2*day, 1));
|
||||
expected.add(new CheckmarkRecord(habit, from + day, 0));
|
||||
expected.add(new CheckmarkRecord(habit, from, 2));
|
||||
|
||||
assertThat(actual, equalTo(expected));
|
||||
}
|
||||
|
||||
private List<CheckmarkRecord> getAllRecords()
|
||||
{
|
||||
return new Select()
|
||||
|
||||
@@ -125,17 +125,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
assertThat(habits.get(3).getName(), equalTo("habit 3"));
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testGetAll_withoutArchived()
|
||||
// {
|
||||
// List<Habit> habits = habitList.toList();
|
||||
// assertThat(habits.size(), equalTo(5));
|
||||
// assertThat(habits.get(3).getName(), equalTo("habit 7"));
|
||||
//
|
||||
// List<Habit> another = habitList.toList();
|
||||
// assertThat(habits, equalTo(another));
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void testGetById()
|
||||
{
|
||||
@@ -178,45 +167,6 @@ public class SQLiteHabitListTest extends BaseAndroidTest
|
||||
assertThat(habitList.indexOf(h2), equalTo(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_reorder()
|
||||
{
|
||||
// Same as HabitListTest.java
|
||||
// TODO: remove duplication
|
||||
|
||||
int operations[][] = {
|
||||
{5, 2}, {3, 7}, {4, 4}, {3, 2}
|
||||
};
|
||||
|
||||
int expectedPosition[][] = {
|
||||
{0, 1, 3, 4, 5, 2, 6, 7, 8, 9},
|
||||
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
|
||||
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
|
||||
{0, 1, 7, 2, 4, 3, 5, 6, 8, 9},
|
||||
};
|
||||
|
||||
for (int i = 0; i < operations.length; i++)
|
||||
{
|
||||
int from = operations[i][0];
|
||||
int to = operations[i][1];
|
||||
|
||||
Habit fromHabit = habitList.getByPosition(from);
|
||||
Habit toHabit = habitList.getByPosition(to);
|
||||
habitList.reorder(fromHabit, toHabit);
|
||||
|
||||
int actualPositions[] = new int[10];
|
||||
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
Habit h = habitList.getById(j);
|
||||
assertNotNull(h);
|
||||
actualPositions[j] = habitList.indexOf(h);
|
||||
}
|
||||
|
||||
assertThat(actualPositions, equalTo(expectedPosition[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private HabitRecord getRecord(long id)
|
||||
{
|
||||
return new Select()
|
||||
|
||||
@@ -61,29 +61,6 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
day = DateUtils.millisecondsInOneDay;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAll()
|
||||
{
|
||||
List<Score> list = scores.toList();
|
||||
assertThat(list.size(), equalTo(121));
|
||||
assertThat(list.get(0).getTimestamp(), equalTo(today));
|
||||
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidateNewerThan()
|
||||
{
|
||||
scores.getTodayValue(); // force recompute
|
||||
List<ScoreRecord> records = getAllRecords();
|
||||
assertThat(records.size(), equalTo(121));
|
||||
|
||||
scores.invalidateNewerThan(today - 10 * day);
|
||||
|
||||
records = getAllRecords();
|
||||
assertThat(records.size(), equalTo(110));
|
||||
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd()
|
||||
{
|
||||
@@ -101,6 +78,15 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
assertThat(records.get(0).timestamp, equalTo(today));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAll()
|
||||
{
|
||||
List<Score> list = scores.toList();
|
||||
assertThat(list.size(), equalTo(121));
|
||||
assertThat(list.get(0).getTimestamp(), equalTo(today));
|
||||
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByInterval()
|
||||
{
|
||||
@@ -115,6 +101,16 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByInterval_concurrent() throws Exception
|
||||
{
|
||||
Runnable block1 = () -> scores.invalidateNewerThan(0);
|
||||
Runnable block2 =
|
||||
() -> assertThat(scores.getByInterval(today, today).size(),
|
||||
equalTo(1));
|
||||
runConcurrently(block1, block2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByInterval_withLongInterval()
|
||||
{
|
||||
@@ -125,6 +121,30 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
assertThat(list.size(), equalTo(201));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTodayValue_concurrent() throws Exception
|
||||
{
|
||||
Runnable block1 = () -> scores.invalidateNewerThan(0);
|
||||
Runnable block2 =
|
||||
() -> assertThat(scores.getTodayValue(), equalTo(18407827));
|
||||
|
||||
runConcurrently(block1, block2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidateNewerThan()
|
||||
{
|
||||
scores.getTodayValue(); // force recompute
|
||||
List<ScoreRecord> records = getAllRecords();
|
||||
assertThat(records.size(), equalTo(121));
|
||||
|
||||
scores.invalidateNewerThan(today - 10 * day);
|
||||
|
||||
records = getAllRecords();
|
||||
assertThat(records.size(), equalTo(110));
|
||||
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
|
||||
}
|
||||
|
||||
private List<ScoreRecord> getAllRecords()
|
||||
{
|
||||
return new Select()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.performance;
|
||||
|
||||
import android.support.test.filters.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.junit.*;
|
||||
|
||||
@MediumTest
|
||||
public class PerformanceTest extends BaseAndroidTest
|
||||
{
|
||||
private Habit habit;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
habit = fixtures.createLongHabit();
|
||||
}
|
||||
|
||||
@Test(timeout = 1000)
|
||||
public void testRepeatedGetTodayValue()
|
||||
{
|
||||
for (int i = 0; i < 100000; i++)
|
||||
{
|
||||
habit.getScores().getTodayValue();
|
||||
habit.getCheckmarks().getTodayValue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.pebble;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.test.runner.*;
|
||||
import android.test.suitebuilder.annotation.*;
|
||||
|
||||
import com.getpebble.android.kit.*;
|
||||
import com.getpebble.android.kit.util.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.receivers.*;
|
||||
import org.json.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
import static com.getpebble.android.kit.Constants.*;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.hamcrest.core.IsEqual.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@MediumTest
|
||||
public class PebbleReceiverTest extends BaseAndroidTest
|
||||
{
|
||||
|
||||
private Habit habit1;
|
||||
|
||||
private Habit habit2;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
|
||||
fixtures.purgeHabits(habitList);
|
||||
|
||||
habit1 = fixtures.createEmptyHabit();
|
||||
habit1.setName("Exercise");
|
||||
|
||||
habit2 = fixtures.createEmptyHabit();
|
||||
habit2.setName("Meditate");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCount() throws Exception
|
||||
{
|
||||
onPebbleReceived((dict) -> {
|
||||
assertThat(dict.getString(0), equalTo("COUNT"));
|
||||
assertThat(dict.getInteger(1), equalTo(2L));
|
||||
});
|
||||
|
||||
PebbleDictionary dict = buildCountRequest();
|
||||
sendFromPebbleToAndroid(dict);
|
||||
awaitLatch();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetch() throws Exception
|
||||
{
|
||||
onPebbleReceived((dict) -> {
|
||||
assertThat(dict.getString(0), equalTo("HABIT"));
|
||||
assertThat(dict.getInteger(1), equalTo(habit2.getId()));
|
||||
assertThat(dict.getString(2), equalTo(habit2.getName()));
|
||||
assertThat(dict.getInteger(3), equalTo(0L));
|
||||
});
|
||||
|
||||
PebbleDictionary dict = buildFetchRequest(1);
|
||||
sendFromPebbleToAndroid(dict);
|
||||
awaitLatch();
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testToggle() throws Exception
|
||||
// {
|
||||
// int v = habit1.getCheckmarks().getTodayValue();
|
||||
// assertThat(v, equalTo(Checkmark.UNCHECKED));
|
||||
//
|
||||
// onPebbleReceived((dict) -> {
|
||||
// assertThat(dict.getString(0), equalTo("OK"));
|
||||
// int value = habit1.getCheckmarks().getTodayValue();
|
||||
// assertThat(value, equalTo(200)); //Checkmark.CHECKED_EXPLICITLY));
|
||||
// });
|
||||
//
|
||||
// PebbleDictionary dict = buildToggleRequest(habit1.getId());
|
||||
// sendFromPebbleToAndroid(dict);
|
||||
// awaitLatch();
|
||||
// }
|
||||
|
||||
@NonNull
|
||||
protected PebbleDictionary buildCountRequest()
|
||||
{
|
||||
PebbleDictionary dict = new PebbleDictionary();
|
||||
dict.addString(0, "COUNT");
|
||||
return dict;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected PebbleDictionary buildFetchRequest(int position)
|
||||
{
|
||||
PebbleDictionary dict = new PebbleDictionary();
|
||||
dict.addString(0, "FETCH");
|
||||
dict.addInt32(1, position);
|
||||
return dict;
|
||||
}
|
||||
|
||||
protected void onPebbleReceived(PebbleProcessor processor)
|
||||
{
|
||||
BroadcastReceiver pebbleReceiver = new BroadcastReceiver()
|
||||
{
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
try
|
||||
{
|
||||
String jsonData = intent.getStringExtra(MSG_DATA);
|
||||
PebbleDictionary dict = PebbleDictionary.fromJson(jsonData);
|
||||
processor.process(dict);
|
||||
latch.countDown();
|
||||
targetContext.unregisterReceiver(this);
|
||||
}
|
||||
catch (JSONException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter(Constants.INTENT_APP_SEND);
|
||||
targetContext.registerReceiver(pebbleReceiver, filter);
|
||||
}
|
||||
|
||||
protected void sendFromPebbleToAndroid(PebbleDictionary dict)
|
||||
{
|
||||
Intent intent = new Intent(Constants.INTENT_APP_RECEIVE);
|
||||
intent.putExtra(Constants.APP_UUID, PebbleReceiver.WATCHAPP_UUID);
|
||||
intent.putExtra(Constants.TRANSACTION_ID, 0);
|
||||
intent.putExtra(Constants.MSG_DATA, dict.toJsonString());
|
||||
targetContext.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private PebbleDictionary buildToggleRequest(long habitId)
|
||||
{
|
||||
PebbleDictionary dict = new PebbleDictionary();
|
||||
dict.addString(0, "TOGGLE");
|
||||
dict.addInt32(1, (int) habitId);
|
||||
return dict;
|
||||
}
|
||||
|
||||
interface PebbleProcessor
|
||||
{
|
||||
void process(PebbleDictionary dict);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class ExportCSVTaskTest extends BaseAndroidTest
|
||||
for (Habit h : habitList) selected.add(h);
|
||||
|
||||
taskRunner.execute(
|
||||
new ExportCSVTask(habitList, selected, archiveFilename -> {
|
||||
new ExportCSVTask(targetContext,habitList, selected, archiveFilename -> {
|
||||
assertThat(archiveFilename, is(not(nullValue())));
|
||||
File f = new File(archiveFilename);
|
||||
assertTrue(f.exists());
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ExportDBTaskTest extends BaseAndroidTest
|
||||
@Test
|
||||
public void testExportCSV() throws Throwable
|
||||
{
|
||||
ExportDBTask task = new ExportDBTask(filename -> {
|
||||
ExportDBTask task = new ExportDBTask(targetContext, filename -> {
|
||||
assertThat(filename, is(not(nullValue())));
|
||||
|
||||
File f = new File(filename);
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<manifest
|
||||
package="org.isoron.uhabits"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="23"
|
||||
android:versionName="1.6.0">
|
||||
android:versionCode="38"
|
||||
android:versionName="1.7.11">
|
||||
|
||||
<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>
|
||||
|
||||
3
app/src/main/assets/migrations/15.sql
Normal file
3
app/src/main/assets/migrations/15.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
delete from Score;
|
||||
delete from Streak;
|
||||
delete from Checkmarks;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 49 KiB |
86
app/src/main/java/com/activeandroid/ActiveAndroid.java
Normal file
86
app/src/main/java/com/activeandroid/ActiveAndroid.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.activeandroid.util.Log;
|
||||
|
||||
public final class ActiveAndroid {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static void initialize(Context context) {
|
||||
initialize(new Configuration.Builder(context).create());
|
||||
}
|
||||
|
||||
public static void initialize(Configuration configuration) {
|
||||
initialize(configuration, false);
|
||||
}
|
||||
|
||||
public static void initialize(Context context, boolean loggingEnabled) {
|
||||
initialize(new Configuration.Builder(context).create(), loggingEnabled);
|
||||
}
|
||||
|
||||
public static void initialize(Configuration configuration, boolean loggingEnabled) {
|
||||
// Set logging enabled first
|
||||
setLoggingEnabled(loggingEnabled);
|
||||
Cache.initialize(configuration);
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
Cache.clear();
|
||||
}
|
||||
|
||||
public static void dispose() {
|
||||
Cache.dispose();
|
||||
}
|
||||
|
||||
public static void setLoggingEnabled(boolean enabled) {
|
||||
Log.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public static SQLiteDatabase getDatabase() {
|
||||
return Cache.openDatabase();
|
||||
}
|
||||
|
||||
public static void beginTransaction() {
|
||||
Cache.openDatabase().beginTransaction();
|
||||
}
|
||||
|
||||
public static void endTransaction() {
|
||||
Cache.openDatabase().endTransaction();
|
||||
}
|
||||
|
||||
public static void setTransactionSuccessful() {
|
||||
Cache.openDatabase().setTransactionSuccessful();
|
||||
}
|
||||
|
||||
public static boolean inTransaction() {
|
||||
return Cache.openDatabase().inTransaction();
|
||||
}
|
||||
|
||||
public static void execSQL(String sql) {
|
||||
Cache.openDatabase().execSQL(sql);
|
||||
}
|
||||
|
||||
public static void execSQL(String sql, Object[] bindArgs) {
|
||||
Cache.openDatabase().execSQL(sql, bindArgs);
|
||||
}
|
||||
}
|
||||
158
app/src/main/java/com/activeandroid/Cache.java
Normal file
158
app/src/main/java/com/activeandroid/Cache.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.support.v4.util.LruCache;
|
||||
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
import com.activeandroid.util.Log;
|
||||
|
||||
public final class Cache {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC CONSTANTS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final int DEFAULT_CACHE_SIZE = 1024;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static Context sContext;
|
||||
|
||||
private static ModelInfo sModelInfo;
|
||||
private static DatabaseHelper sDatabaseHelper;
|
||||
|
||||
private static LruCache<String, Model> sEntities;
|
||||
|
||||
private static boolean sIsInitialized = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Cache() {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static synchronized void initialize(Configuration configuration) {
|
||||
if (sIsInitialized) {
|
||||
Log.v("ActiveAndroid already initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
sContext = configuration.getContext();
|
||||
sModelInfo = new ModelInfo(configuration);
|
||||
sDatabaseHelper = new DatabaseHelper(configuration);
|
||||
|
||||
// TODO: It would be nice to override sizeOf here and calculate the memory
|
||||
// actually used, however at this point it seems like the reflection
|
||||
// required would be too costly to be of any benefit. We'll just set a max
|
||||
// object size instead.
|
||||
sEntities = new LruCache<String, Model>(configuration.getCacheSize());
|
||||
|
||||
openDatabase();
|
||||
|
||||
sIsInitialized = true;
|
||||
|
||||
Log.v("ActiveAndroid initialized successfully.");
|
||||
}
|
||||
|
||||
public static synchronized void clear() {
|
||||
sEntities.evictAll();
|
||||
Log.v("Cache cleared.");
|
||||
}
|
||||
|
||||
public static synchronized void dispose() {
|
||||
closeDatabase();
|
||||
|
||||
sEntities = null;
|
||||
sModelInfo = null;
|
||||
sDatabaseHelper = null;
|
||||
|
||||
sIsInitialized = false;
|
||||
|
||||
Log.v("ActiveAndroid disposed. Call initialize to use library.");
|
||||
}
|
||||
|
||||
// Database access
|
||||
|
||||
public static boolean isInitialized() {
|
||||
return sIsInitialized;
|
||||
}
|
||||
|
||||
public static synchronized SQLiteDatabase openDatabase() {
|
||||
return sDatabaseHelper.getWritableDatabase();
|
||||
}
|
||||
|
||||
public static synchronized void closeDatabase() {
|
||||
sDatabaseHelper.close();
|
||||
}
|
||||
|
||||
// Context access
|
||||
|
||||
public static Context getContext() {
|
||||
return sContext;
|
||||
}
|
||||
|
||||
// Entity cache
|
||||
|
||||
public static String getIdentifier(Class<? extends Model> type, Long id) {
|
||||
return getTableName(type) + "@" + id;
|
||||
}
|
||||
|
||||
public static String getIdentifier(Model entity) {
|
||||
return getIdentifier(entity.getClass(), entity.getId());
|
||||
}
|
||||
|
||||
public static synchronized void addEntity(Model entity) {
|
||||
sEntities.put(getIdentifier(entity), entity);
|
||||
}
|
||||
|
||||
public static synchronized Model getEntity(Class<? extends Model> type, long id) {
|
||||
return sEntities.get(getIdentifier(type, id));
|
||||
}
|
||||
|
||||
public static synchronized void removeEntity(Model entity) {
|
||||
sEntities.remove(getIdentifier(entity));
|
||||
}
|
||||
|
||||
// Model cache
|
||||
|
||||
public static synchronized Collection<TableInfo> getTableInfos() {
|
||||
return sModelInfo.getTableInfos();
|
||||
}
|
||||
|
||||
public static synchronized TableInfo getTableInfo(Class<? extends Model> type) {
|
||||
return sModelInfo.getTableInfo(type);
|
||||
}
|
||||
|
||||
public static synchronized TypeSerializer getParserForType(Class<?> type) {
|
||||
return sModelInfo.getTypeSerializer(type);
|
||||
}
|
||||
|
||||
public static synchronized String getTableName(Class<? extends Model> type) {
|
||||
return sModelInfo.getTableInfo(type).getTableName();
|
||||
}
|
||||
}
|
||||
318
app/src/main/java/com/activeandroid/Configuration.java
Normal file
318
app/src/main/java/com/activeandroid/Configuration.java
Normal file
@@ -0,0 +1,318 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
import com.activeandroid.util.Log;
|
||||
import com.activeandroid.util.ReflectionUtils;
|
||||
|
||||
public class Configuration {
|
||||
|
||||
public final static String SQL_PARSER_LEGACY = "legacy";
|
||||
public final static String SQL_PARSER_DELIMITED = "delimited";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Context mContext;
|
||||
private String mDatabaseName;
|
||||
private int mDatabaseVersion;
|
||||
private String mSqlParser;
|
||||
private List<Class<? extends Model>> mModelClasses;
|
||||
private List<Class<? extends TypeSerializer>> mTypeSerializers;
|
||||
private int mCacheSize;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Configuration(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public String getDatabaseName() {
|
||||
return mDatabaseName;
|
||||
}
|
||||
|
||||
public int getDatabaseVersion() {
|
||||
return mDatabaseVersion;
|
||||
}
|
||||
|
||||
public String getSqlParser() {
|
||||
return mSqlParser;
|
||||
}
|
||||
|
||||
public List<Class<? extends Model>> getModelClasses() {
|
||||
return mModelClasses;
|
||||
}
|
||||
|
||||
public List<Class<? extends TypeSerializer>> getTypeSerializers() {
|
||||
return mTypeSerializers;
|
||||
}
|
||||
|
||||
public int getCacheSize() {
|
||||
return mCacheSize;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return mModelClasses != null && mModelClasses.size() > 0;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// INNER CLASSES
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static class Builder {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE CONSTANTS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final String AA_DB_NAME = "AA_DB_NAME";
|
||||
private static final String AA_DB_VERSION = "AA_DB_VERSION";
|
||||
private final static String AA_MODELS = "AA_MODELS";
|
||||
private final static String AA_SERIALIZERS = "AA_SERIALIZERS";
|
||||
private final static String AA_SQL_PARSER = "AA_SQL_PARSER";
|
||||
|
||||
private static final int DEFAULT_CACHE_SIZE = 1024;
|
||||
private static final String DEFAULT_DB_NAME = "Application.db";
|
||||
private static final String DEFAULT_SQL_PARSER = SQL_PARSER_LEGACY;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Context mContext;
|
||||
|
||||
private Integer mCacheSize;
|
||||
private String mDatabaseName;
|
||||
private Integer mDatabaseVersion;
|
||||
private String mSqlParser;
|
||||
private List<Class<? extends Model>> mModelClasses;
|
||||
private List<Class<? extends TypeSerializer>> mTypeSerializers;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Builder(Context context) {
|
||||
mContext = context.getApplicationContext();
|
||||
mCacheSize = DEFAULT_CACHE_SIZE;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Builder setCacheSize(int cacheSize) {
|
||||
mCacheSize = cacheSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDatabaseName(String databaseName) {
|
||||
mDatabaseName = databaseName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDatabaseVersion(int databaseVersion) {
|
||||
mDatabaseVersion = databaseVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSqlParser(String sqlParser) {
|
||||
mSqlParser = sqlParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addModelClass(Class<? extends Model> modelClass) {
|
||||
if (mModelClasses == null) {
|
||||
mModelClasses = new ArrayList<Class<? extends Model>>();
|
||||
}
|
||||
|
||||
mModelClasses.add(modelClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addModelClasses(Class<? extends Model>... modelClasses) {
|
||||
if (mModelClasses == null) {
|
||||
mModelClasses = new ArrayList<Class<? extends Model>>();
|
||||
}
|
||||
|
||||
mModelClasses.addAll(Arrays.asList(modelClasses));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setModelClasses(Class<? extends Model>... modelClasses) {
|
||||
mModelClasses = Arrays.asList(modelClasses);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addTypeSerializer(Class<? extends TypeSerializer> typeSerializer) {
|
||||
if (mTypeSerializers == null) {
|
||||
mTypeSerializers = new ArrayList<Class<? extends TypeSerializer>>();
|
||||
}
|
||||
|
||||
mTypeSerializers.add(typeSerializer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addTypeSerializers(Class<? extends TypeSerializer>... typeSerializers) {
|
||||
if (mTypeSerializers == null) {
|
||||
mTypeSerializers = new ArrayList<Class<? extends TypeSerializer>>();
|
||||
}
|
||||
|
||||
mTypeSerializers.addAll(Arrays.asList(typeSerializers));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTypeSerializers(Class<? extends TypeSerializer>... typeSerializers) {
|
||||
mTypeSerializers = Arrays.asList(typeSerializers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Configuration create() {
|
||||
Configuration configuration = new Configuration(mContext);
|
||||
configuration.mCacheSize = mCacheSize;
|
||||
|
||||
// Get database name from meta-data
|
||||
if (mDatabaseName != null) {
|
||||
configuration.mDatabaseName = mDatabaseName;
|
||||
} else {
|
||||
configuration.mDatabaseName = getMetaDataDatabaseNameOrDefault();
|
||||
}
|
||||
|
||||
// Get database version from meta-data
|
||||
if (mDatabaseVersion != null) {
|
||||
configuration.mDatabaseVersion = mDatabaseVersion;
|
||||
} else {
|
||||
configuration.mDatabaseVersion = getMetaDataDatabaseVersionOrDefault();
|
||||
}
|
||||
|
||||
// Get SQL parser from meta-data
|
||||
if (mSqlParser != null) {
|
||||
configuration.mSqlParser = mSqlParser;
|
||||
} else {
|
||||
configuration.mSqlParser = getMetaDataSqlParserOrDefault();
|
||||
}
|
||||
|
||||
// Get model classes from meta-data
|
||||
if (mModelClasses != null) {
|
||||
configuration.mModelClasses = mModelClasses;
|
||||
} else {
|
||||
final String modelList = ReflectionUtils.getMetaData(mContext, AA_MODELS);
|
||||
if (modelList != null) {
|
||||
configuration.mModelClasses = loadModelList(modelList.split(","));
|
||||
}
|
||||
}
|
||||
|
||||
// Get type serializer classes from meta-data
|
||||
if (mTypeSerializers != null) {
|
||||
configuration.mTypeSerializers = mTypeSerializers;
|
||||
} else {
|
||||
final String serializerList = ReflectionUtils.getMetaData(mContext, AA_SERIALIZERS);
|
||||
if (serializerList != null) {
|
||||
configuration.mTypeSerializers = loadSerializerList(serializerList.split(","));
|
||||
}
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Meta-data methods
|
||||
|
||||
private String getMetaDataDatabaseNameOrDefault() {
|
||||
String aaName = ReflectionUtils.getMetaData(mContext, AA_DB_NAME);
|
||||
if (aaName == null) {
|
||||
aaName = DEFAULT_DB_NAME;
|
||||
}
|
||||
|
||||
return aaName;
|
||||
}
|
||||
|
||||
private int getMetaDataDatabaseVersionOrDefault() {
|
||||
Integer aaVersion = ReflectionUtils.getMetaData(mContext, AA_DB_VERSION);
|
||||
if (aaVersion == null || aaVersion == 0) {
|
||||
aaVersion = 1;
|
||||
}
|
||||
|
||||
return aaVersion;
|
||||
}
|
||||
|
||||
private String getMetaDataSqlParserOrDefault() {
|
||||
final String mode = ReflectionUtils.getMetaData(mContext, AA_SQL_PARSER);
|
||||
if (mode == null) {
|
||||
return DEFAULT_SQL_PARSER;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
private List<Class<? extends Model>> loadModelList(String[] models) {
|
||||
final List<Class<? extends Model>> modelClasses = new ArrayList<Class<? extends Model>>();
|
||||
final ClassLoader classLoader = mContext.getClass().getClassLoader();
|
||||
for (String model : models) {
|
||||
try {
|
||||
Class modelClass = Class.forName(model.trim(), false, classLoader);
|
||||
if (ReflectionUtils.isModel(modelClass)) {
|
||||
modelClasses.add(modelClass);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
Log.e("Couldn't create class.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return modelClasses;
|
||||
}
|
||||
|
||||
private List<Class<? extends TypeSerializer>> loadSerializerList(String[] serializers) {
|
||||
final List<Class<? extends TypeSerializer>> typeSerializers = new ArrayList<Class<? extends TypeSerializer>>();
|
||||
final ClassLoader classLoader = mContext.getClass().getClassLoader();
|
||||
for (String serializer : serializers) {
|
||||
try {
|
||||
Class serializerClass = Class.forName(serializer.trim(), false, classLoader);
|
||||
if (ReflectionUtils.isTypeSerializer(serializerClass)) {
|
||||
typeSerializers.add(serializerClass);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
Log.e("Couldn't create class.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return typeSerializers;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
257
app/src/main/java/com/activeandroid/DatabaseHelper.java
Normal file
257
app/src/main/java/com/activeandroid/DatabaseHelper.java
Normal file
@@ -0,0 +1,257 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.activeandroid.util.IOUtils;
|
||||
import com.activeandroid.util.Log;
|
||||
import com.activeandroid.util.NaturalOrderComparator;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
import com.activeandroid.util.SqlParser;
|
||||
|
||||
public final class DatabaseHelper extends SQLiteOpenHelper {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC CONSTANTS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public final static String MIGRATION_PATH = "migrations";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE FIELDS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private final String mSqlParser;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public DatabaseHelper(Configuration configuration) {
|
||||
super(configuration.getContext(), configuration.getDatabaseName(), null, configuration.getDatabaseVersion());
|
||||
copyAttachedDatabase(configuration.getContext(), configuration.getDatabaseName());
|
||||
mSqlParser = configuration.getSqlParser();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// OVERRIDEN METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onOpen(SQLiteDatabase db) {
|
||||
executePragmas(db);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
executePragmas(db);
|
||||
executeCreate(db);
|
||||
executeMigrations(db, -1, db.getVersion());
|
||||
executeCreateIndex(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
executePragmas(db);
|
||||
executeCreate(db);
|
||||
executeMigrations(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void copyAttachedDatabase(Context context, String databaseName) {
|
||||
final File dbPath = context.getDatabasePath(databaseName);
|
||||
|
||||
// If the database already exists, return
|
||||
if (dbPath.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have a path to the file
|
||||
dbPath.getParentFile().mkdirs();
|
||||
|
||||
// Try to copy database file
|
||||
try {
|
||||
final InputStream inputStream = context.getAssets().open(databaseName);
|
||||
final OutputStream output = new FileOutputStream(dbPath);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int length;
|
||||
|
||||
while ((length = inputStream.read(buffer, 0, 8192)) > 0) {
|
||||
output.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
output.flush();
|
||||
output.close();
|
||||
inputStream.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
Log.e("Failed to open file", e);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void executePragmas(SQLiteDatabase db) {
|
||||
if (SQLiteUtils.FOREIGN_KEYS_SUPPORTED) {
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
Log.i("Foreign Keys supported. Enabling foreign key features.");
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCreateIndex(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (TableInfo tableInfo : Cache.getTableInfos()) {
|
||||
String[] definitions = SQLiteUtils.createIndexDefinition(tableInfo);
|
||||
|
||||
for (String definition : definitions) {
|
||||
db.execSQL(definition);
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
}
|
||||
finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCreate(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (TableInfo tableInfo : Cache.getTableInfos()) {
|
||||
db.execSQL(SQLiteUtils.createTableDefinition(tableInfo));
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
}
|
||||
finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean executeMigrations(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
boolean migrationExecuted = false;
|
||||
try {
|
||||
final List<String> files = Arrays.asList(Cache.getContext().getAssets().list(MIGRATION_PATH));
|
||||
Collections.sort(files, new NaturalOrderComparator());
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (String file : files) {
|
||||
try {
|
||||
final int version = Integer.valueOf(file.replace(".sql", ""));
|
||||
|
||||
if (version > oldVersion && version <= newVersion) {
|
||||
executeSqlScript(db, file);
|
||||
migrationExecuted = true;
|
||||
|
||||
Log.i(file + " executed succesfully.");
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
Log.w("Skipping invalidly named file: " + file, e);
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
}
|
||||
finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Log.e("Failed to execute migrations.", e);
|
||||
}
|
||||
|
||||
return migrationExecuted;
|
||||
}
|
||||
|
||||
private void executeSqlScript(SQLiteDatabase db, String file) {
|
||||
|
||||
InputStream stream = null;
|
||||
|
||||
try {
|
||||
stream = Cache.getContext().getAssets().open(MIGRATION_PATH + "/" + file);
|
||||
|
||||
if (Configuration.SQL_PARSER_DELIMITED.equalsIgnoreCase(mSqlParser)) {
|
||||
executeDelimitedSqlScript(db, stream);
|
||||
|
||||
} else {
|
||||
executeLegacySqlScript(db, stream);
|
||||
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e("Failed to execute " + file, e);
|
||||
|
||||
} finally {
|
||||
IOUtils.closeQuietly(stream);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void executeDelimitedSqlScript(SQLiteDatabase db, InputStream stream) throws IOException {
|
||||
|
||||
List<String> commands = SqlParser.parse(stream);
|
||||
|
||||
for(String command : commands) {
|
||||
db.execSQL(command);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeLegacySqlScript(SQLiteDatabase db, InputStream stream) throws IOException {
|
||||
|
||||
InputStreamReader reader = null;
|
||||
BufferedReader buffer = null;
|
||||
|
||||
try {
|
||||
reader = new InputStreamReader(stream);
|
||||
buffer = new BufferedReader(reader);
|
||||
String line = null;
|
||||
|
||||
while ((line = buffer.readLine()) != null) {
|
||||
line = line.replace(";", "").trim();
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
db.execSQL(line);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
IOUtils.closeQuietly(buffer);
|
||||
IOUtils.closeQuietly(reader);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
314
app/src/main/java/com/activeandroid/Model.java
Normal file
314
app/src/main/java/com/activeandroid/Model.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
import com.activeandroid.util.Log;
|
||||
import com.activeandroid.util.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class Model {
|
||||
|
||||
/** Prime number used for hashcode() implementation. */
|
||||
private static final int HASH_PRIME = 739;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Long mId = null;
|
||||
|
||||
private final TableInfo mTableInfo;
|
||||
private final String idName;
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Model() {
|
||||
mTableInfo = Cache.getTableInfo(getClass());
|
||||
idName = mTableInfo.getIdName();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public final Long getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public final void delete() {
|
||||
Cache.openDatabase().delete(mTableInfo.getTableName(), idName+"=?", new String[] { getId().toString() });
|
||||
Cache.removeEntity(this);
|
||||
}
|
||||
|
||||
public final Long save() {
|
||||
final SQLiteDatabase db = Cache.openDatabase();
|
||||
final ContentValues values = new ContentValues();
|
||||
|
||||
for (Field field : mTableInfo.getFields()) {
|
||||
final String fieldName = mTableInfo.getColumnName(field);
|
||||
Class<?> fieldType = field.getType();
|
||||
|
||||
field.setAccessible(true);
|
||||
|
||||
try {
|
||||
Object value = field.get(this);
|
||||
|
||||
if (value != null) {
|
||||
final TypeSerializer typeSerializer = Cache.getParserForType(fieldType);
|
||||
if (typeSerializer != null) {
|
||||
// serialize data
|
||||
value = typeSerializer.serialize(value);
|
||||
// set new object type
|
||||
if (value != null) {
|
||||
fieldType = value.getClass();
|
||||
// check that the serializer returned what it promised
|
||||
if (!fieldType.equals(typeSerializer.getSerializedType())) {
|
||||
Log.w(String.format("TypeSerializer returned wrong type: expected a %s but got a %s",
|
||||
typeSerializer.getSerializedType(), fieldType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Find a smarter way to do this? This if block is necessary because we
|
||||
// can't know the type until runtime.
|
||||
if (value == null) {
|
||||
values.putNull(fieldName);
|
||||
}
|
||||
else if (fieldType.equals(Byte.class) || fieldType.equals(byte.class)) {
|
||||
values.put(fieldName, (Byte) value);
|
||||
}
|
||||
else if (fieldType.equals(Short.class) || fieldType.equals(short.class)) {
|
||||
values.put(fieldName, (Short) value);
|
||||
}
|
||||
else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) {
|
||||
values.put(fieldName, (Integer) value);
|
||||
}
|
||||
else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) {
|
||||
values.put(fieldName, (Long) value);
|
||||
}
|
||||
else if (fieldType.equals(Float.class) || fieldType.equals(float.class)) {
|
||||
values.put(fieldName, (Float) value);
|
||||
}
|
||||
else if (fieldType.equals(Double.class) || fieldType.equals(double.class)) {
|
||||
values.put(fieldName, (Double) value);
|
||||
}
|
||||
else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) {
|
||||
values.put(fieldName, (Boolean) value);
|
||||
}
|
||||
else if (fieldType.equals(Character.class) || fieldType.equals(char.class)) {
|
||||
values.put(fieldName, value.toString());
|
||||
}
|
||||
else if (fieldType.equals(String.class)) {
|
||||
values.put(fieldName, value.toString());
|
||||
}
|
||||
else if (fieldType.equals(Byte[].class) || fieldType.equals(byte[].class)) {
|
||||
values.put(fieldName, (byte[]) value);
|
||||
}
|
||||
else if (ReflectionUtils.isModel(fieldType)) {
|
||||
values.put(fieldName, ((Model) value).getId());
|
||||
}
|
||||
else if (ReflectionUtils.isSubclassOf(fieldType, Enum.class)) {
|
||||
values.put(fieldName, ((Enum<?>) value).name());
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
Log.e(e.getClass().getName(), e);
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
Log.e(e.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (mId == null) {
|
||||
mId = db.insert(mTableInfo.getTableName(), null, values);
|
||||
}
|
||||
else {
|
||||
db.update(mTableInfo.getTableName(), values, idName+"=" + mId, null);
|
||||
}
|
||||
|
||||
return mId;
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
|
||||
public static void delete(Class<? extends Model> type, long id) {
|
||||
TableInfo tableInfo = Cache.getTableInfo(type);
|
||||
new Delete().from(type).where(tableInfo.getIdName()+"=?", id).execute();
|
||||
}
|
||||
|
||||
public static <T extends Model> T load(Class<T> type, long id) {
|
||||
TableInfo tableInfo = Cache.getTableInfo(type);
|
||||
return (T) new Select().from(type).where(tableInfo.getIdName()+"=?", id).executeSingle();
|
||||
}
|
||||
|
||||
// Model population
|
||||
|
||||
public final void loadFromCursor(Cursor cursor) {
|
||||
/**
|
||||
* Obtain the columns ordered to fix issue #106 (https://github.com/pardom/ActiveAndroid/issues/106)
|
||||
* when the cursor have multiple columns with same name obtained from join tables.
|
||||
*/
|
||||
List<String> columnsOrdered = new ArrayList<String>(Arrays.asList(cursor.getColumnNames()));
|
||||
for (Field field : mTableInfo.getFields()) {
|
||||
final String fieldName = mTableInfo.getColumnName(field);
|
||||
Class<?> fieldType = field.getType();
|
||||
final int columnIndex = columnsOrdered.indexOf(fieldName);
|
||||
|
||||
if (columnIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
|
||||
try {
|
||||
boolean columnIsNull = cursor.isNull(columnIndex);
|
||||
TypeSerializer typeSerializer = Cache.getParserForType(fieldType);
|
||||
Object value = null;
|
||||
|
||||
if (typeSerializer != null) {
|
||||
fieldType = typeSerializer.getSerializedType();
|
||||
}
|
||||
|
||||
// TODO: Find a smarter way to do this? This if block is necessary because we
|
||||
// can't know the type until runtime.
|
||||
if (columnIsNull) {
|
||||
field = null;
|
||||
}
|
||||
else if (fieldType.equals(Byte.class) || fieldType.equals(byte.class)) {
|
||||
value = cursor.getInt(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Short.class) || fieldType.equals(short.class)) {
|
||||
value = cursor.getInt(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) {
|
||||
value = cursor.getInt(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) {
|
||||
value = cursor.getLong(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Float.class) || fieldType.equals(float.class)) {
|
||||
value = cursor.getFloat(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Double.class) || fieldType.equals(double.class)) {
|
||||
value = cursor.getDouble(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) {
|
||||
value = cursor.getInt(columnIndex) != 0;
|
||||
}
|
||||
else if (fieldType.equals(Character.class) || fieldType.equals(char.class)) {
|
||||
value = cursor.getString(columnIndex).charAt(0);
|
||||
}
|
||||
else if (fieldType.equals(String.class)) {
|
||||
value = cursor.getString(columnIndex);
|
||||
}
|
||||
else if (fieldType.equals(Byte[].class) || fieldType.equals(byte[].class)) {
|
||||
value = cursor.getBlob(columnIndex);
|
||||
}
|
||||
else if (ReflectionUtils.isModel(fieldType)) {
|
||||
final long entityId = cursor.getLong(columnIndex);
|
||||
final Class<? extends Model> entityType = (Class<? extends Model>) fieldType;
|
||||
|
||||
Model entity = Cache.getEntity(entityType, entityId);
|
||||
if (entity == null) {
|
||||
entity = new Select().from(entityType).where(idName+"=?", entityId).executeSingle();
|
||||
}
|
||||
|
||||
value = entity;
|
||||
}
|
||||
else if (ReflectionUtils.isSubclassOf(fieldType, Enum.class)) {
|
||||
@SuppressWarnings("rawtypes")
|
||||
final Class<? extends Enum> enumType = (Class<? extends Enum>) fieldType;
|
||||
value = Enum.valueOf(enumType, cursor.getString(columnIndex));
|
||||
}
|
||||
|
||||
// Use a deserializer if one is available
|
||||
if (typeSerializer != null && !columnIsNull) {
|
||||
value = typeSerializer.deserialize(value);
|
||||
}
|
||||
|
||||
// Set the field value
|
||||
if (value != null) {
|
||||
field.set(this, value);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
Log.e(e.getClass().getName(), e);
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
Log.e(e.getClass().getName(), e);
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
Log.e(e.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (mId != null) {
|
||||
Cache.addEntity(this);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTECTED METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected final <T extends Model> List<T> getMany(Class<T> type, String foreignKey) {
|
||||
return new Select().from(type).where(Cache.getTableName(type) + "." + foreignKey + "=?", getId()).execute();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// OVERRIDEN METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mTableInfo.getTableName() + "@" + getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Model && this.mId != null) {
|
||||
final Model other = (Model) obj;
|
||||
|
||||
return this.mId.equals(other.mId)
|
||||
&& (this.mTableInfo.getTableName().equals(other.mTableInfo.getTableName()));
|
||||
} else {
|
||||
return this == obj;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = HASH_PRIME;
|
||||
hash += HASH_PRIME * (mId == null ? super.hashCode() : mId.hashCode()); //if id is null, use Object.hashCode()
|
||||
hash += HASH_PRIME * mTableInfo.getTableName().hashCode();
|
||||
return hash; //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
}
|
||||
209
app/src/main/java/com/activeandroid/ModelInfo.java
Normal file
209
app/src/main/java/com/activeandroid/ModelInfo.java
Normal file
@@ -0,0 +1,209 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.activeandroid.serializer.CalendarSerializer;
|
||||
import com.activeandroid.serializer.SqlDateSerializer;
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
import com.activeandroid.serializer.UtilDateSerializer;
|
||||
import com.activeandroid.serializer.FileSerializer;
|
||||
import com.activeandroid.util.Log;
|
||||
import com.activeandroid.util.ReflectionUtils;
|
||||
import dalvik.system.DexFile;
|
||||
|
||||
final class ModelInfo {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Map<Class<? extends Model>, TableInfo> mTableInfos = new HashMap<Class<? extends Model>, TableInfo>();
|
||||
private Map<Class<?>, TypeSerializer> mTypeSerializers = new HashMap<Class<?>, TypeSerializer>() {
|
||||
{
|
||||
put(Calendar.class, new CalendarSerializer());
|
||||
put(java.sql.Date.class, new SqlDateSerializer());
|
||||
put(java.util.Date.class, new UtilDateSerializer());
|
||||
put(java.io.File.class, new FileSerializer());
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public ModelInfo(Configuration configuration) {
|
||||
if (!loadModelFromMetaData(configuration)) {
|
||||
try {
|
||||
scanForModel(configuration.getContext());
|
||||
}
|
||||
catch (IOException e) {
|
||||
Log.e("Couldn't open source path.", e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("ModelInfo loaded.");
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Collection<TableInfo> getTableInfos() {
|
||||
return mTableInfos.values();
|
||||
}
|
||||
|
||||
public TableInfo getTableInfo(Class<? extends Model> type) {
|
||||
return mTableInfos.get(type);
|
||||
}
|
||||
|
||||
public TypeSerializer getTypeSerializer(Class<?> type) {
|
||||
return mTypeSerializers.get(type);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private boolean loadModelFromMetaData(Configuration configuration) {
|
||||
if (!configuration.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<Class<? extends Model>> models = configuration.getModelClasses();
|
||||
if (models != null) {
|
||||
for (Class<? extends Model> model : models) {
|
||||
mTableInfos.put(model, new TableInfo(model));
|
||||
}
|
||||
}
|
||||
|
||||
final List<Class<? extends TypeSerializer>> typeSerializers = configuration.getTypeSerializers();
|
||||
if (typeSerializers != null) {
|
||||
for (Class<? extends TypeSerializer> typeSerializer : typeSerializers) {
|
||||
try {
|
||||
TypeSerializer instance = typeSerializer.newInstance();
|
||||
mTypeSerializers.put(instance.getDeserializedType(), instance);
|
||||
}
|
||||
catch (InstantiationException e) {
|
||||
Log.e("Couldn't instantiate TypeSerializer.", e);
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
Log.e("IllegalAccessException", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scanForModel(Context context) throws IOException {
|
||||
String packageName = context.getPackageName();
|
||||
String sourcePath = context.getApplicationInfo().sourceDir;
|
||||
List<String> paths = new ArrayList<String>();
|
||||
|
||||
if (sourcePath != null && !(new File(sourcePath).isDirectory())) {
|
||||
DexFile dexfile = new DexFile(sourcePath);
|
||||
Enumeration<String> entries = dexfile.entries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
paths.add(entries.nextElement());
|
||||
}
|
||||
}
|
||||
// Robolectric fallback
|
||||
else {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
Enumeration<URL> resources = classLoader.getResources("");
|
||||
|
||||
while (resources.hasMoreElements()) {
|
||||
String path = resources.nextElement().getFile();
|
||||
if (path.contains("bin") || path.contains("classes")) {
|
||||
paths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String path : paths) {
|
||||
File file = new File(path);
|
||||
scanForModelClasses(file, packageName, context.getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
private void scanForModelClasses(File path, String packageName, ClassLoader classLoader) {
|
||||
if (path.isDirectory()) {
|
||||
for (File file : path.listFiles()) {
|
||||
scanForModelClasses(file, packageName, classLoader);
|
||||
}
|
||||
}
|
||||
else {
|
||||
String className = path.getName();
|
||||
|
||||
// Robolectric fallback
|
||||
if (!path.getPath().equals(className)) {
|
||||
className = path.getPath();
|
||||
|
||||
if (className.endsWith(".class")) {
|
||||
className = className.substring(0, className.length() - 6);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
className = className.replace(System.getProperty("file.separator"), ".");
|
||||
|
||||
int packageNameIndex = className.lastIndexOf(packageName);
|
||||
if (packageNameIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
className = className.substring(packageNameIndex);
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> discoveredClass = Class.forName(className, false, classLoader);
|
||||
if (ReflectionUtils.isModel(discoveredClass)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends Model> modelClass = (Class<? extends Model>) discoveredClass;
|
||||
mTableInfos.put(modelClass, new TableInfo(modelClass));
|
||||
}
|
||||
else if (ReflectionUtils.isTypeSerializer(discoveredClass)) {
|
||||
TypeSerializer instance = (TypeSerializer) discoveredClass.newInstance();
|
||||
mTypeSerializers.put(instance.getDeserializedType(), instance);
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
Log.e("Couldn't create class.", e);
|
||||
}
|
||||
catch (InstantiationException e) {
|
||||
Log.e("Couldn't instantiate TypeSerializer.", e);
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
Log.e("IllegalAccessException", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
app/src/main/java/com/activeandroid/TableInfo.java
Normal file
124
app/src/main/java/com/activeandroid/TableInfo.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.activeandroid;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.util.ReflectionUtils;
|
||||
|
||||
public final class TableInfo {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Class<? extends Model> mType;
|
||||
private String mTableName;
|
||||
private String mIdName = Table.DEFAULT_ID_NAME;
|
||||
|
||||
private Map<Field, String> mColumnNames = new LinkedHashMap<Field, String>();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public TableInfo(Class<? extends Model> type) {
|
||||
mType = type;
|
||||
|
||||
final Table tableAnnotation = type.getAnnotation(Table.class);
|
||||
|
||||
if (tableAnnotation != null) {
|
||||
mTableName = tableAnnotation.name();
|
||||
mIdName = tableAnnotation.id();
|
||||
}
|
||||
else {
|
||||
mTableName = type.getSimpleName();
|
||||
}
|
||||
|
||||
// Manually add the id column since it is not declared like the other columns.
|
||||
Field idField = getIdField(type);
|
||||
mColumnNames.put(idField, mIdName);
|
||||
|
||||
List<Field> fields = new LinkedList<Field>(ReflectionUtils.getDeclaredColumnFields(type));
|
||||
Collections.reverse(fields);
|
||||
|
||||
for (Field field : fields) {
|
||||
if (field.isAnnotationPresent(Column.class)) {
|
||||
final Column columnAnnotation = field.getAnnotation(Column.class);
|
||||
String columnName = columnAnnotation.name();
|
||||
if (TextUtils.isEmpty(columnName)) {
|
||||
columnName = field.getName();
|
||||
}
|
||||
|
||||
mColumnNames.put(field, columnName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Class<? extends Model> getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public String getTableName() {
|
||||
return mTableName;
|
||||
}
|
||||
|
||||
public String getIdName() {
|
||||
return mIdName;
|
||||
}
|
||||
|
||||
public Collection<Field> getFields() {
|
||||
return mColumnNames.keySet();
|
||||
}
|
||||
|
||||
public String getColumnName(Field field) {
|
||||
return mColumnNames.get(field);
|
||||
}
|
||||
|
||||
|
||||
private Field getIdField(Class<?> type) {
|
||||
if (type.equals(Model.class)) {
|
||||
try {
|
||||
return type.getDeclaredField("mId");
|
||||
}
|
||||
catch (NoSuchFieldException e) {
|
||||
Log.e("Impossible!", e.toString());
|
||||
}
|
||||
}
|
||||
else if (type.getSuperclass() != null) {
|
||||
return getIdField(type.getSuperclass());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
110
app/src/main/java/com/activeandroid/annotation/Column.java
Normal file
110
app/src/main/java/com/activeandroid/annotation/Column.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.activeandroid.annotation;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Column {
|
||||
public enum ConflictAction {
|
||||
ROLLBACK, ABORT, FAIL, IGNORE, REPLACE
|
||||
}
|
||||
|
||||
public enum ForeignKeyAction {
|
||||
SET_NULL, SET_DEFAULT, CASCADE, RESTRICT, NO_ACTION
|
||||
}
|
||||
|
||||
public String name() default "";
|
||||
|
||||
public int length() default -1;
|
||||
|
||||
public boolean notNull() default false;
|
||||
|
||||
public ConflictAction onNullConflict() default ConflictAction.FAIL;
|
||||
|
||||
public ForeignKeyAction onDelete() default ForeignKeyAction.NO_ACTION;
|
||||
|
||||
public ForeignKeyAction onUpdate() default ForeignKeyAction.NO_ACTION;
|
||||
|
||||
public boolean unique() default false;
|
||||
|
||||
public ConflictAction onUniqueConflict() default ConflictAction.FAIL;
|
||||
|
||||
/*
|
||||
* If set uniqueGroups = {"group_name"}, we will create a table constraint with group.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* @Table(name = "table_name")
|
||||
* public class Table extends Model {
|
||||
* @Column(name = "member1", uniqueGroups = {"group1"}, onUniqueConflicts = {ConflictAction.FAIL})
|
||||
* public String member1;
|
||||
*
|
||||
* @Column(name = "member2", uniqueGroups = {"group1", "group2"}, onUniqueConflicts = {ConflictAction.FAIL, ConflictAction.IGNORE})
|
||||
* public String member2;
|
||||
*
|
||||
* @Column(name = "member3", uniqueGroups = {"group2"}, onUniqueConflicts = {ConflictAction.IGNORE})
|
||||
* public String member3;
|
||||
* }
|
||||
*
|
||||
* CREATE TABLE table_name (..., UNIQUE (member1, member2) ON CONFLICT FAIL, UNIQUE (member2, member3) ON CONFLICT IGNORE)
|
||||
*/
|
||||
public String[] uniqueGroups() default {};
|
||||
|
||||
public ConflictAction[] onUniqueConflicts() default {};
|
||||
|
||||
/*
|
||||
* If set index = true, we will create a index with single column.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* @Table(name = "table_name")
|
||||
* public class Table extends Model {
|
||||
* @Column(name = "member", index = true)
|
||||
* public String member;
|
||||
* }
|
||||
*
|
||||
* Execute CREATE INDEX index_table_name_member on table_name(member)
|
||||
*/
|
||||
public boolean index() default false;
|
||||
|
||||
/*
|
||||
* If set indexGroups = {"group_name"}, we will create a index with group.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* @Table(name = "table_name")
|
||||
* public class Table extends Model {
|
||||
* @Column(name = "member1", indexGroups = {"group1"})
|
||||
* public String member1;
|
||||
*
|
||||
* @Column(name = "member2", indexGroups = {"group1", "group2"})
|
||||
* public String member2;
|
||||
*
|
||||
* @Column(name = "member3", indexGroups = {"group2"})
|
||||
* public String member3;
|
||||
* }
|
||||
*
|
||||
* Execute CREATE INDEX index_table_name_group1 on table_name(member1, member2)
|
||||
* Execute CREATE INDEX index_table_name_group2 on table_name(member2, member3)
|
||||
*/
|
||||
public String[] indexGroups() default {};
|
||||
}
|
||||
31
app/src/main/java/com/activeandroid/annotation/Table.java
Normal file
31
app/src/main/java/com/activeandroid/annotation/Table.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.activeandroid.annotation;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Table {
|
||||
|
||||
public static final String DEFAULT_ID_NAME = "Id";
|
||||
public String name();
|
||||
public String id() default DEFAULT_ID_NAME;
|
||||
}
|
||||
33
app/src/main/java/com/activeandroid/query/Delete.java
Normal file
33
app/src/main/java/com/activeandroid/query/Delete.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.activeandroid.Model;
|
||||
|
||||
public final class Delete implements Sqlable {
|
||||
public Delete() {
|
||||
}
|
||||
|
||||
public From from(Class<? extends Model> table) {
|
||||
return new From(table, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
return "DELETE ";
|
||||
}
|
||||
}
|
||||
344
app/src/main/java/com/activeandroid/query/From.java
Normal file
344
app/src/main/java/com/activeandroid/query/From.java
Normal file
@@ -0,0 +1,344 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.query.Join.JoinType;
|
||||
import com.activeandroid.util.Log;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class From implements Sqlable {
|
||||
private Sqlable mQueryBase;
|
||||
|
||||
private Class<? extends Model> mType;
|
||||
private String mAlias;
|
||||
private List<Join> mJoins;
|
||||
private final StringBuilder mWhere = new StringBuilder();
|
||||
private String mGroupBy;
|
||||
private String mHaving;
|
||||
private String mOrderBy;
|
||||
private String mLimit;
|
||||
private String mOffset;
|
||||
|
||||
private List<Object> mArguments;
|
||||
|
||||
public From(Class<? extends Model> table, Sqlable queryBase) {
|
||||
mType = table;
|
||||
mJoins = new ArrayList<Join>();
|
||||
mQueryBase = queryBase;
|
||||
|
||||
mJoins = new ArrayList<Join>();
|
||||
mArguments = new ArrayList<Object>();
|
||||
}
|
||||
|
||||
public From as(String alias) {
|
||||
mAlias = alias;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Join join(Class<? extends Model> table) {
|
||||
Join join = new Join(this, table, null);
|
||||
mJoins.add(join);
|
||||
return join;
|
||||
}
|
||||
|
||||
public Join leftJoin(Class<? extends Model> table) {
|
||||
Join join = new Join(this, table, JoinType.LEFT);
|
||||
mJoins.add(join);
|
||||
return join;
|
||||
}
|
||||
|
||||
public Join outerJoin(Class<? extends Model> table) {
|
||||
Join join = new Join(this, table, JoinType.OUTER);
|
||||
mJoins.add(join);
|
||||
return join;
|
||||
}
|
||||
|
||||
public Join innerJoin(Class<? extends Model> table) {
|
||||
Join join = new Join(this, table, JoinType.INNER);
|
||||
mJoins.add(join);
|
||||
return join;
|
||||
}
|
||||
|
||||
public Join crossJoin(Class<? extends Model> table) {
|
||||
Join join = new Join(this, table, JoinType.CROSS);
|
||||
mJoins.add(join);
|
||||
return join;
|
||||
}
|
||||
|
||||
public From where(String clause) {
|
||||
// Chain conditions if a previous condition exists.
|
||||
if (mWhere.length() > 0) {
|
||||
mWhere.append(" AND ");
|
||||
}
|
||||
mWhere.append(clause);
|
||||
return this;
|
||||
}
|
||||
|
||||
public From where(String clause, Object... args) {
|
||||
where(clause).addArguments(args);
|
||||
return this;
|
||||
}
|
||||
|
||||
public From and(String clause) {
|
||||
return where(clause);
|
||||
}
|
||||
|
||||
public From and(String clause, Object... args) {
|
||||
return where(clause, args);
|
||||
}
|
||||
|
||||
public From or(String clause) {
|
||||
if (mWhere.length() > 0) {
|
||||
mWhere.append(" OR ");
|
||||
}
|
||||
mWhere.append(clause);
|
||||
return this;
|
||||
}
|
||||
|
||||
public From or(String clause, Object... args) {
|
||||
or(clause).addArguments(args);
|
||||
return this;
|
||||
}
|
||||
|
||||
public From groupBy(String groupBy) {
|
||||
mGroupBy = groupBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public From having(String having) {
|
||||
mHaving = having;
|
||||
return this;
|
||||
}
|
||||
|
||||
public From orderBy(String orderBy) {
|
||||
mOrderBy = orderBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public From limit(int limit) {
|
||||
return limit(String.valueOf(limit));
|
||||
}
|
||||
|
||||
public From limit(String limit) {
|
||||
mLimit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public From offset(int offset) {
|
||||
return offset(String.valueOf(offset));
|
||||
}
|
||||
|
||||
public From offset(String offset) {
|
||||
mOffset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
void addArguments(Object[] args) {
|
||||
for(Object arg : args) {
|
||||
if (arg.getClass() == boolean.class || arg.getClass() == Boolean.class) {
|
||||
arg = (arg.equals(true) ? 1 : 0);
|
||||
}
|
||||
mArguments.add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
private void addFrom(final StringBuilder sql) {
|
||||
sql.append("FROM ");
|
||||
sql.append(Cache.getTableName(mType)).append(" ");
|
||||
|
||||
if (mAlias != null) {
|
||||
sql.append("AS ");
|
||||
sql.append(mAlias);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addJoins(final StringBuilder sql) {
|
||||
for (final Join join : mJoins) {
|
||||
sql.append(join.toSql());
|
||||
}
|
||||
}
|
||||
|
||||
private void addWhere(final StringBuilder sql) {
|
||||
if (mWhere.length() > 0) {
|
||||
sql.append("WHERE ");
|
||||
sql.append(mWhere);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroupBy(final StringBuilder sql) {
|
||||
if (mGroupBy != null) {
|
||||
sql.append("GROUP BY ");
|
||||
sql.append(mGroupBy);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addHaving(final StringBuilder sql) {
|
||||
if (mHaving != null) {
|
||||
sql.append("HAVING ");
|
||||
sql.append(mHaving);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addOrderBy(final StringBuilder sql) {
|
||||
if (mOrderBy != null) {
|
||||
sql.append("ORDER BY ");
|
||||
sql.append(mOrderBy);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addLimit(final StringBuilder sql) {
|
||||
if (mLimit != null) {
|
||||
sql.append("LIMIT ");
|
||||
sql.append(mLimit);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private void addOffset(final StringBuilder sql) {
|
||||
if (mOffset != null) {
|
||||
sql.append("OFFSET ");
|
||||
sql.append(mOffset);
|
||||
sql.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private String sqlString(final StringBuilder sql) {
|
||||
|
||||
final String sqlString = sql.toString().trim();
|
||||
|
||||
// Don't waste time building the string
|
||||
// unless we're going to log it.
|
||||
if (Log.isEnabled()) {
|
||||
Log.v(sqlString + " " + TextUtils.join(",", getArguments()));
|
||||
}
|
||||
|
||||
return sqlString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
final StringBuilder sql = new StringBuilder();
|
||||
sql.append(mQueryBase.toSql());
|
||||
|
||||
addFrom(sql);
|
||||
addJoins(sql);
|
||||
addWhere(sql);
|
||||
addGroupBy(sql);
|
||||
addHaving(sql);
|
||||
addOrderBy(sql);
|
||||
addLimit(sql);
|
||||
addOffset(sql);
|
||||
|
||||
return sqlString(sql);
|
||||
}
|
||||
|
||||
public String toExistsSql() {
|
||||
|
||||
final StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT EXISTS(SELECT 1 ");
|
||||
|
||||
addFrom(sql);
|
||||
addJoins(sql);
|
||||
addWhere(sql);
|
||||
addGroupBy(sql);
|
||||
addHaving(sql);
|
||||
addLimit(sql);
|
||||
addOffset(sql);
|
||||
|
||||
sql.append(")");
|
||||
|
||||
return sqlString(sql);
|
||||
}
|
||||
|
||||
public String toCountSql() {
|
||||
|
||||
final StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT COUNT(*) ");
|
||||
|
||||
addFrom(sql);
|
||||
addJoins(sql);
|
||||
addWhere(sql);
|
||||
addGroupBy(sql);
|
||||
addHaving(sql);
|
||||
addLimit(sql);
|
||||
addOffset(sql);
|
||||
|
||||
return sqlString(sql);
|
||||
}
|
||||
|
||||
public <T extends Model> List<T> execute() {
|
||||
if (mQueryBase instanceof Select) {
|
||||
return SQLiteUtils.rawQuery(mType, toSql(), getArguments());
|
||||
|
||||
} else {
|
||||
SQLiteUtils.execSql(toSql(), getArguments());
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public <T extends Model> T executeSingle() {
|
||||
if (mQueryBase instanceof Select) {
|
||||
limit(1);
|
||||
return (T) SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments());
|
||||
|
||||
} else {
|
||||
limit(1);
|
||||
SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments()).delete();
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value indicating whether the query returns any rows.
|
||||
* @return <code>true</code> if the query returns at least one row; otherwise, <code>false</code>.
|
||||
*/
|
||||
public boolean exists() {
|
||||
return SQLiteUtils.intQuery(toExistsSql(), getArguments()) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of rows returned by the query.
|
||||
*/
|
||||
public int count() {
|
||||
return SQLiteUtils.intQuery(toCountSql(), getArguments());
|
||||
}
|
||||
|
||||
public String[] getArguments() {
|
||||
final int size = mArguments.size();
|
||||
final String[] args = new String[size];
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
args[i] = mArguments.get(i).toString();
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
94
app/src/main/java/com/activeandroid/query/Join.java
Normal file
94
app/src/main/java/com/activeandroid/query/Join.java
Normal file
@@ -0,0 +1,94 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.Model;
|
||||
|
||||
public final class Join implements Sqlable {
|
||||
static enum JoinType {
|
||||
LEFT, OUTER, INNER, CROSS
|
||||
}
|
||||
|
||||
private From mFrom;
|
||||
private Class<? extends Model> mType;
|
||||
private String mAlias;
|
||||
private JoinType mJoinType;
|
||||
private String mOn;
|
||||
private String[] mUsing;
|
||||
|
||||
Join(From from, Class<? extends Model> table, JoinType joinType) {
|
||||
mFrom = from;
|
||||
mType = table;
|
||||
mJoinType = joinType;
|
||||
}
|
||||
|
||||
public Join as(String alias) {
|
||||
mAlias = alias;
|
||||
return this;
|
||||
}
|
||||
|
||||
public From on(String on) {
|
||||
mOn = on;
|
||||
return mFrom;
|
||||
}
|
||||
|
||||
public From on(String on, Object... args) {
|
||||
mOn = on;
|
||||
mFrom.addArguments(args);
|
||||
return mFrom;
|
||||
}
|
||||
|
||||
public From using(String... columns) {
|
||||
mUsing = columns;
|
||||
return mFrom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
|
||||
if (mJoinType != null) {
|
||||
sql.append(mJoinType.toString()).append(" ");
|
||||
}
|
||||
|
||||
sql.append("JOIN ");
|
||||
sql.append(Cache.getTableName(mType));
|
||||
sql.append(" ");
|
||||
|
||||
if (mAlias != null) {
|
||||
sql.append("AS ");
|
||||
sql.append(mAlias);
|
||||
sql.append(" ");
|
||||
}
|
||||
|
||||
if (mOn != null) {
|
||||
sql.append("ON ");
|
||||
sql.append(mOn);
|
||||
sql.append(" ");
|
||||
}
|
||||
else if (mUsing != null) {
|
||||
sql.append("USING (");
|
||||
sql.append(TextUtils.join(", ", mUsing));
|
||||
sql.append(") ");
|
||||
}
|
||||
|
||||
return sql.toString();
|
||||
}
|
||||
}
|
||||
93
app/src/main/java/com/activeandroid/query/Select.java
Normal file
93
app/src/main/java/com/activeandroid/query/Select.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
|
||||
public final class Select implements Sqlable {
|
||||
private String[] mColumns;
|
||||
private boolean mDistinct = false;
|
||||
private boolean mAll = false;
|
||||
|
||||
public Select() {
|
||||
}
|
||||
|
||||
public Select(String... columns) {
|
||||
mColumns = columns;
|
||||
}
|
||||
|
||||
public Select(Column... columns) {
|
||||
final int size = columns.length;
|
||||
mColumns = new String[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
mColumns[i] = columns[i].name + " AS " + columns[i].alias;
|
||||
}
|
||||
}
|
||||
|
||||
public Select distinct() {
|
||||
mDistinct = true;
|
||||
mAll = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Select all() {
|
||||
mDistinct = false;
|
||||
mAll = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public From from(Class<? extends Model> table) {
|
||||
return new From(table, this);
|
||||
}
|
||||
|
||||
public static class Column {
|
||||
String name;
|
||||
String alias;
|
||||
|
||||
public Column(String name, String alias) {
|
||||
this.name = name;
|
||||
this.alias = alias;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
|
||||
sql.append("SELECT ");
|
||||
|
||||
if (mDistinct) {
|
||||
sql.append("DISTINCT ");
|
||||
}
|
||||
else if (mAll) {
|
||||
sql.append("ALL ");
|
||||
}
|
||||
|
||||
if (mColumns != null && mColumns.length > 0) {
|
||||
sql.append(TextUtils.join(", ", mColumns) + " ");
|
||||
}
|
||||
else {
|
||||
sql.append("* ");
|
||||
}
|
||||
|
||||
return sql.toString();
|
||||
}
|
||||
}
|
||||
103
app/src/main/java/com/activeandroid/query/Set.java
Normal file
103
app/src/main/java/com/activeandroid/query/Set.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class Set implements Sqlable {
|
||||
private Update mUpdate;
|
||||
|
||||
private String mSet;
|
||||
private String mWhere;
|
||||
|
||||
private List<Object> mSetArguments;
|
||||
private List<Object> mWhereArguments;
|
||||
|
||||
public Set(Update queryBase, String set) {
|
||||
mUpdate = queryBase;
|
||||
mSet = set;
|
||||
|
||||
mSetArguments = new ArrayList<Object>();
|
||||
mWhereArguments = new ArrayList<Object>();
|
||||
}
|
||||
|
||||
public Set(Update queryBase, String set, Object... args) {
|
||||
mUpdate = queryBase;
|
||||
mSet = set;
|
||||
|
||||
mSetArguments = new ArrayList<Object>();
|
||||
mWhereArguments = new ArrayList<Object>();
|
||||
|
||||
mSetArguments.addAll(Arrays.asList(args));
|
||||
}
|
||||
|
||||
public Set where(String where) {
|
||||
mWhere = where;
|
||||
mWhereArguments.clear();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Set where(String where, Object... args) {
|
||||
mWhere = where;
|
||||
mWhereArguments.clear();
|
||||
mWhereArguments.addAll(Arrays.asList(args));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append(mUpdate.toSql());
|
||||
sql.append("SET ");
|
||||
sql.append(mSet);
|
||||
sql.append(" ");
|
||||
|
||||
if (mWhere != null) {
|
||||
sql.append("WHERE ");
|
||||
sql.append(mWhere);
|
||||
sql.append(" ");
|
||||
}
|
||||
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
SQLiteUtils.execSql(toSql(), getArguments());
|
||||
}
|
||||
|
||||
public String[] getArguments() {
|
||||
final int setSize = mSetArguments.size();
|
||||
final int whereSize = mWhereArguments.size();
|
||||
final String[] args = new String[setSize + whereSize];
|
||||
|
||||
for (int i = 0; i < setSize; i++) {
|
||||
args[i] = mSetArguments.get(i).toString();
|
||||
}
|
||||
|
||||
for (int i = 0; i < whereSize; i++) {
|
||||
args[i + setSize] = mWhereArguments.get(i).toString();
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/com/activeandroid/query/Sqlable.java
Normal file
21
app/src/main/java/com/activeandroid/query/Sqlable.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public interface Sqlable {
|
||||
public String toSql();
|
||||
}
|
||||
50
app/src/main/java/com/activeandroid/query/Update.java
Normal file
50
app/src/main/java/com/activeandroid/query/Update.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.activeandroid.query;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.Model;
|
||||
|
||||
public final class Update implements Sqlable {
|
||||
private Class<? extends Model> mType;
|
||||
|
||||
public Update(Class<? extends Model> table) {
|
||||
mType = table;
|
||||
}
|
||||
|
||||
public Set set(String set) {
|
||||
return new Set(this, set);
|
||||
}
|
||||
|
||||
public Set set(String set, Object... args) {
|
||||
return new Set(this, set, args);
|
||||
}
|
||||
|
||||
Class<? extends Model> getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("UPDATE ");
|
||||
sql.append(Cache.getTableName(mType));
|
||||
sql.append(" ");
|
||||
|
||||
return sql.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class BigDecimalSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return BigDecimal.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
public String serialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((BigDecimal) data).toString();
|
||||
}
|
||||
|
||||
public BigDecimal deserialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BigDecimal((String) data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public final class CalendarSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return Calendar.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return long.class;
|
||||
}
|
||||
|
||||
public Long serialize(Object data) {
|
||||
return ((Calendar) data).getTimeInMillis();
|
||||
}
|
||||
|
||||
public Calendar deserialize(Object data) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis((Long) data);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
public final class FileSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return File.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
public String serialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((File) data).toString();
|
||||
}
|
||||
|
||||
public File deserialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File((String) data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
public final class SqlDateSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return Date.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return long.class;
|
||||
}
|
||||
|
||||
public Long serialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((Date) data).getTime();
|
||||
}
|
||||
|
||||
public Date deserialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date((Long) data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public abstract class TypeSerializer {
|
||||
public abstract Class<?> getDeserializedType();
|
||||
|
||||
public abstract Class<?> getSerializedType();
|
||||
|
||||
public abstract Object serialize(Object data);
|
||||
|
||||
public abstract Object deserialize(Object data);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class UUIDSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return UUID.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
public String serialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((UUID) data).toString();
|
||||
}
|
||||
|
||||
public UUID deserialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UUID.fromString((String)data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.activeandroid.serializer;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public final class UtilDateSerializer extends TypeSerializer {
|
||||
public Class<?> getDeserializedType() {
|
||||
return Date.class;
|
||||
}
|
||||
|
||||
public Class<?> getSerializedType() {
|
||||
return long.class;
|
||||
}
|
||||
|
||||
public Long serialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((Date) data).getTime();
|
||||
}
|
||||
|
||||
public Date deserialize(Object data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date((Long) data);
|
||||
}
|
||||
}
|
||||
71
app/src/main/java/com/activeandroid/util/IOUtils.java
Normal file
71
app/src/main/java/com/activeandroid/util/IOUtils.java
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014 Markus Pfeiffer
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.activeandroid.util.Log;
|
||||
|
||||
|
||||
public class IOUtils {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Unconditionally close a {@link Closeable}.
|
||||
* </p>
|
||||
* Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is
|
||||
* typically used in finally blocks.
|
||||
* @param closeable A {@link Closeable} to close.
|
||||
*/
|
||||
public static void closeQuietly(final Closeable closeable) {
|
||||
|
||||
if (closeable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (final IOException e) {
|
||||
Log.e("Couldn't close closeable.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Unconditionally close a {@link Cursor}.
|
||||
* </p>
|
||||
* Equivalent to {@link Cursor#close()}, except any exceptions will be ignored. This is
|
||||
* typically used in finally blocks.
|
||||
* @param cursor A {@link Cursor} to close.
|
||||
*/
|
||||
public static void closeQuietly(final Cursor cursor) {
|
||||
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cursor.close();
|
||||
} catch (final Exception e) {
|
||||
Log.e("Couldn't close cursor.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
196
app/src/main/java/com/activeandroid/util/Log.java
Normal file
196
app/src/main/java/com/activeandroid/util/Log.java
Normal file
@@ -0,0 +1,196 @@
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public final class Log {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static String sTag = "ActiveAndroid";
|
||||
private static boolean sEnabled = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTRUCTORS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Log() {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return sEnabled;
|
||||
}
|
||||
|
||||
public static void setEnabled(boolean enabled) {
|
||||
sEnabled = enabled;
|
||||
}
|
||||
|
||||
public static boolean isLoggingEnabled() {
|
||||
return sEnabled;
|
||||
}
|
||||
|
||||
public static int v(String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.v(sTag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.v(tag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int v(String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.v(sTag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.v(tag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int d(String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.d(sTag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.d(tag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int d(String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.d(sTag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.d(tag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int i(String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.i(sTag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.i(tag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int i(String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.i(sTag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.i(tag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int w(String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.w(sTag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.w(tag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int w(String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.w(sTag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.w(tag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int e(String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.e(sTag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.e(tag, msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int e(String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.e(sTag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.e(tag, msg, tr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int t(String msg, Object... args) {
|
||||
if (sEnabled) {
|
||||
return android.util.Log.v("test", String.format(msg, args));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java.
|
||||
Copyright (C) 2003 by Pierre-Luc Paour <natorder@paour.com>
|
||||
|
||||
Based on the C version by Martin Pool, of which this is more or less a straight conversion.
|
||||
Copyright (C) 2000 by Martin Pool <mbp@humbug.org.au>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class NaturalOrderComparator implements Comparator<Object> {
|
||||
int compareRight(String a, String b) {
|
||||
int bias = 0;
|
||||
int ia = 0;
|
||||
int ib = 0;
|
||||
|
||||
// The longest run of digits wins. That aside, the greatest
|
||||
// value wins, but we can't know that it will until we've scanned
|
||||
// both numbers to know that they have the same magnitude, so we
|
||||
// remember it in BIAS.
|
||||
for (;; ia++, ib++) {
|
||||
char ca = charAt(a, ia);
|
||||
char cb = charAt(b, ib);
|
||||
|
||||
if (!Character.isDigit(ca) && !Character.isDigit(cb)) {
|
||||
return bias;
|
||||
}
|
||||
else if (!Character.isDigit(ca)) {
|
||||
return -1;
|
||||
}
|
||||
else if (!Character.isDigit(cb)) {
|
||||
return +1;
|
||||
}
|
||||
else if (ca < cb) {
|
||||
if (bias == 0) {
|
||||
bias = -1;
|
||||
}
|
||||
}
|
||||
else if (ca > cb) {
|
||||
if (bias == 0)
|
||||
bias = +1;
|
||||
}
|
||||
else if (ca == 0 && cb == 0) {
|
||||
return bias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int compare(Object o1, Object o2) {
|
||||
String a = o1.toString();
|
||||
String b = o2.toString();
|
||||
|
||||
int ia = 0, ib = 0;
|
||||
int nza = 0, nzb = 0;
|
||||
char ca, cb;
|
||||
int result;
|
||||
|
||||
while (true) {
|
||||
// only count the number of zeroes leading the last number compared
|
||||
nza = nzb = 0;
|
||||
|
||||
ca = charAt(a, ia);
|
||||
cb = charAt(b, ib);
|
||||
|
||||
// skip over leading spaces or zeros
|
||||
while (Character.isSpaceChar(ca) || ca == '0') {
|
||||
if (ca == '0') {
|
||||
nza++;
|
||||
}
|
||||
else {
|
||||
// only count consecutive zeroes
|
||||
nza = 0;
|
||||
}
|
||||
|
||||
ca = charAt(a, ++ia);
|
||||
}
|
||||
|
||||
while (Character.isSpaceChar(cb) || cb == '0') {
|
||||
if (cb == '0') {
|
||||
nzb++;
|
||||
}
|
||||
else {
|
||||
// only count consecutive zeroes
|
||||
nzb = 0;
|
||||
}
|
||||
|
||||
cb = charAt(b, ++ib);
|
||||
}
|
||||
|
||||
// process run of digits
|
||||
if (Character.isDigit(ca) && Character.isDigit(cb)) {
|
||||
if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (ca == 0 && cb == 0) {
|
||||
// The strings compare the same. Perhaps the caller
|
||||
// will want to call strcmp to break the tie.
|
||||
return nza - nzb;
|
||||
}
|
||||
|
||||
if (ca < cb) {
|
||||
return -1;
|
||||
}
|
||||
else if (ca > cb) {
|
||||
return +1;
|
||||
}
|
||||
|
||||
++ia;
|
||||
++ib;
|
||||
}
|
||||
}
|
||||
|
||||
static char charAt(String s, int i) {
|
||||
if (i >= s.length()) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return s.charAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/com/activeandroid/util/ReflectionUtils.java
Normal file
110
app/src/main/java/com/activeandroid/util/ReflectionUtils.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
|
||||
public final class ReflectionUtils {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static boolean isModel(Class<?> type) {
|
||||
return isSubclassOf(type, Model.class) && (!Modifier.isAbstract(type.getModifiers()));
|
||||
}
|
||||
|
||||
public static boolean isTypeSerializer(Class<?> type) {
|
||||
return isSubclassOf(type, TypeSerializer.class);
|
||||
}
|
||||
|
||||
// Meta-data
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getMetaData(Context context, String name) {
|
||||
try {
|
||||
final ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(),
|
||||
PackageManager.GET_META_DATA);
|
||||
|
||||
if (ai.metaData != null) {
|
||||
return (T) ai.metaData.get(name);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.w("Couldn't find meta-data: " + name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Set<Field> getDeclaredColumnFields(Class<?> type) {
|
||||
Set<Field> declaredColumnFields = Collections.emptySet();
|
||||
|
||||
if (ReflectionUtils.isSubclassOf(type, Model.class) || Model.class.equals(type)) {
|
||||
declaredColumnFields = new LinkedHashSet<Field>();
|
||||
|
||||
Field[] fields = type.getDeclaredFields();
|
||||
Arrays.sort(fields, new Comparator<Field>() {
|
||||
@Override
|
||||
public int compare(Field field1, Field field2) {
|
||||
return field2.getName().compareTo(field1.getName());
|
||||
}
|
||||
});
|
||||
for (Field field : fields) {
|
||||
if (field.isAnnotationPresent(Column.class)) {
|
||||
declaredColumnFields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> parentType = type.getSuperclass();
|
||||
if (parentType != null) {
|
||||
declaredColumnFields.addAll(getDeclaredColumnFields(parentType));
|
||||
}
|
||||
}
|
||||
|
||||
return declaredColumnFields;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static boolean isSubclassOf(Class<?> type, Class<?> superClass) {
|
||||
if (type.getSuperclass() != null) {
|
||||
if (type.getSuperclass().equals(superClass)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isSubclassOf(type.getSuperclass(), superClass);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
406
app/src/main/java/com/activeandroid/util/SQLiteUtils.java
Normal file
406
app/src/main/java/com/activeandroid/util/SQLiteUtils.java
Normal file
@@ -0,0 +1,406 @@
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Pardo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.TableInfo;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Column.ConflictAction;
|
||||
import com.activeandroid.serializer.TypeSerializer;
|
||||
|
||||
import java.lang.Long;
|
||||
import java.lang.String;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SQLiteUtils {
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// ENUMERATIONS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public enum SQLiteType {
|
||||
INTEGER, REAL, TEXT, BLOB
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC CONSTANTS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final boolean FOREIGN_KEYS_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE CONTSANTS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final HashMap<Class<?>, SQLiteType> TYPE_MAP = new HashMap<Class<?>, SQLiteType>() {
|
||||
{
|
||||
put(byte.class, SQLiteType.INTEGER);
|
||||
put(short.class, SQLiteType.INTEGER);
|
||||
put(int.class, SQLiteType.INTEGER);
|
||||
put(long.class, SQLiteType.INTEGER);
|
||||
put(float.class, SQLiteType.REAL);
|
||||
put(double.class, SQLiteType.REAL);
|
||||
put(boolean.class, SQLiteType.INTEGER);
|
||||
put(char.class, SQLiteType.TEXT);
|
||||
put(byte[].class, SQLiteType.BLOB);
|
||||
put(Byte.class, SQLiteType.INTEGER);
|
||||
put(Short.class, SQLiteType.INTEGER);
|
||||
put(Integer.class, SQLiteType.INTEGER);
|
||||
put(Long.class, SQLiteType.INTEGER);
|
||||
put(Float.class, SQLiteType.REAL);
|
||||
put(Double.class, SQLiteType.REAL);
|
||||
put(Boolean.class, SQLiteType.INTEGER);
|
||||
put(Character.class, SQLiteType.TEXT);
|
||||
put(String.class, SQLiteType.TEXT);
|
||||
put(Byte[].class, SQLiteType.BLOB);
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRIVATE MEMBERS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static HashMap<String, List<String>> sIndexGroupMap;
|
||||
private static HashMap<String, List<String>> sUniqueGroupMap;
|
||||
private static HashMap<String, ConflictAction> sOnUniqueConflictsMap;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// PUBLIC METHODS
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static void execSql(String sql) {
|
||||
Cache.openDatabase().execSQL(sql);
|
||||
}
|
||||
|
||||
public static void execSql(String sql, Object[] bindArgs) {
|
||||
Cache.openDatabase().execSQL(sql, bindArgs);
|
||||
}
|
||||
|
||||
public static <T extends Model> List<T> rawQuery(Class<? extends Model> type, String sql, String[] selectionArgs) {
|
||||
Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs);
|
||||
List<T> entities = processCursor(type, cursor);
|
||||
cursor.close();
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static int intQuery(final String sql, final String[] selectionArgs) {
|
||||
final Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs);
|
||||
final int number = processIntCursor(cursor);
|
||||
cursor.close();
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
public static <T extends Model> T rawQuerySingle(Class<? extends Model> type, String sql, String[] selectionArgs) {
|
||||
List<T> entities = rawQuery(type, sql, selectionArgs);
|
||||
|
||||
if (entities.size() > 0) {
|
||||
return entities.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Database creation
|
||||
|
||||
public static ArrayList<String> createUniqueDefinition(TableInfo tableInfo) {
|
||||
final ArrayList<String> definitions = new ArrayList<String>();
|
||||
sUniqueGroupMap = new HashMap<String, List<String>>();
|
||||
sOnUniqueConflictsMap = new HashMap<String, ConflictAction>();
|
||||
|
||||
for (Field field : tableInfo.getFields()) {
|
||||
createUniqueColumnDefinition(tableInfo, field);
|
||||
}
|
||||
|
||||
if (sUniqueGroupMap.isEmpty()) {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
Set<String> keySet = sUniqueGroupMap.keySet();
|
||||
for (String key : keySet) {
|
||||
List<String> group = sUniqueGroupMap.get(key);
|
||||
ConflictAction conflictAction = sOnUniqueConflictsMap.get(key);
|
||||
|
||||
definitions.add(String.format("UNIQUE (%s) ON CONFLICT %s",
|
||||
TextUtils.join(", ", group), conflictAction.toString()));
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
public static void createUniqueColumnDefinition(TableInfo tableInfo, Field field) {
|
||||
final String name = tableInfo.getColumnName(field);
|
||||
final Column column = field.getAnnotation(Column.class);
|
||||
|
||||
if (field.getName().equals("mId")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] groups = column.uniqueGroups();
|
||||
ConflictAction[] conflictActions = column.onUniqueConflicts();
|
||||
if (groups.length != conflictActions.length)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
String group = groups[i];
|
||||
ConflictAction conflictAction = conflictActions[i];
|
||||
|
||||
if (TextUtils.isEmpty(group))
|
||||
continue;
|
||||
|
||||
List<String> list = sUniqueGroupMap.get(group);
|
||||
if (list == null) {
|
||||
list = new ArrayList<String>();
|
||||
}
|
||||
list.add(name);
|
||||
|
||||
sUniqueGroupMap.put(group, list);
|
||||
sOnUniqueConflictsMap.put(group, conflictAction);
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] createIndexDefinition(TableInfo tableInfo) {
|
||||
final ArrayList<String> definitions = new ArrayList<String>();
|
||||
sIndexGroupMap = new HashMap<String, List<String>>();
|
||||
|
||||
for (Field field : tableInfo.getFields()) {
|
||||
createIndexColumnDefinition(tableInfo, field);
|
||||
}
|
||||
|
||||
if (sIndexGroupMap.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : sIndexGroupMap.entrySet()) {
|
||||
definitions.add(String.format("CREATE INDEX IF NOT EXISTS %s on %s(%s);",
|
||||
"index_" + tableInfo.getTableName() + "_" + entry.getKey(),
|
||||
tableInfo.getTableName(), TextUtils.join(", ", entry.getValue())));
|
||||
}
|
||||
|
||||
return definitions.toArray(new String[definitions.size()]);
|
||||
}
|
||||
|
||||
public static void createIndexColumnDefinition(TableInfo tableInfo, Field field) {
|
||||
final String name = tableInfo.getColumnName(field);
|
||||
final Column column = field.getAnnotation(Column.class);
|
||||
|
||||
if (field.getName().equals("mId")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (column.index()) {
|
||||
List<String> list = new ArrayList<String>();
|
||||
list.add(name);
|
||||
sIndexGroupMap.put(name, list);
|
||||
}
|
||||
|
||||
String[] groups = column.indexGroups();
|
||||
for (String group : groups) {
|
||||
if (TextUtils.isEmpty(group))
|
||||
continue;
|
||||
|
||||
List<String> list = sIndexGroupMap.get(group);
|
||||
if (list == null) {
|
||||
list = new ArrayList<String>();
|
||||
}
|
||||
|
||||
list.add(name);
|
||||
sIndexGroupMap.put(group, list);
|
||||
}
|
||||
}
|
||||
|
||||
public static String createTableDefinition(TableInfo tableInfo) {
|
||||
final ArrayList<String> definitions = new ArrayList<String>();
|
||||
|
||||
for (Field field : tableInfo.getFields()) {
|
||||
String definition = createColumnDefinition(tableInfo, field);
|
||||
if (!TextUtils.isEmpty(definition)) {
|
||||
definitions.add(definition);
|
||||
}
|
||||
}
|
||||
|
||||
definitions.addAll(createUniqueDefinition(tableInfo));
|
||||
|
||||
return String.format("CREATE TABLE IF NOT EXISTS %s (%s);", tableInfo.getTableName(),
|
||||
TextUtils.join(", ", definitions));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String createColumnDefinition(TableInfo tableInfo, Field field) {
|
||||
StringBuilder definition = new StringBuilder();
|
||||
|
||||
Class<?> type = field.getType();
|
||||
final String name = tableInfo.getColumnName(field);
|
||||
final TypeSerializer typeSerializer = Cache.getParserForType(field.getType());
|
||||
final Column column = field.getAnnotation(Column.class);
|
||||
|
||||
if (typeSerializer != null) {
|
||||
type = typeSerializer.getSerializedType();
|
||||
}
|
||||
|
||||
if (TYPE_MAP.containsKey(type)) {
|
||||
definition.append(name);
|
||||
definition.append(" ");
|
||||
definition.append(TYPE_MAP.get(type).toString());
|
||||
}
|
||||
else if (ReflectionUtils.isModel(type)) {
|
||||
definition.append(name);
|
||||
definition.append(" ");
|
||||
definition.append(SQLiteType.INTEGER.toString());
|
||||
}
|
||||
else if (ReflectionUtils.isSubclassOf(type, Enum.class)) {
|
||||
definition.append(name);
|
||||
definition.append(" ");
|
||||
definition.append(SQLiteType.TEXT.toString());
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(definition)) {
|
||||
|
||||
if (name.equals(tableInfo.getIdName())) {
|
||||
definition.append(" PRIMARY KEY AUTOINCREMENT");
|
||||
}else if(column!=null){
|
||||
if (column.length() > -1) {
|
||||
definition.append("(");
|
||||
definition.append(column.length());
|
||||
definition.append(")");
|
||||
}
|
||||
|
||||
if (column.notNull()) {
|
||||
definition.append(" NOT NULL ON CONFLICT ");
|
||||
definition.append(column.onNullConflict().toString());
|
||||
}
|
||||
|
||||
if (column.unique()) {
|
||||
definition.append(" UNIQUE ON CONFLICT ");
|
||||
definition.append(column.onUniqueConflict().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (FOREIGN_KEYS_SUPPORTED && ReflectionUtils.isModel(type)) {
|
||||
definition.append(" REFERENCES ");
|
||||
definition.append(Cache.getTableInfo((Class<? extends Model>) type).getTableName());
|
||||
definition.append("("+tableInfo.getIdName()+")");
|
||||
definition.append(" ON DELETE ");
|
||||
definition.append(column.onDelete().toString().replace("_", " "));
|
||||
definition.append(" ON UPDATE ");
|
||||
definition.append(column.onUpdate().toString().replace("_", " "));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.e("No type mapping for: " + type.toString());
|
||||
}
|
||||
|
||||
return definition.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends Model> List<T> processCursor(Class<? extends Model> type, Cursor cursor) {
|
||||
TableInfo tableInfo = Cache.getTableInfo(type);
|
||||
String idName = tableInfo.getIdName();
|
||||
final List<T> entities = new ArrayList<T>();
|
||||
|
||||
try {
|
||||
Constructor<?> entityConstructor = type.getConstructor();
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
/**
|
||||
* Obtain the columns ordered to fix issue #106 (https://github.com/pardom/ActiveAndroid/issues/106)
|
||||
* when the cursor have multiple columns with same name obtained from join tables.
|
||||
*/
|
||||
List<String> columnsOrdered = new ArrayList<String>(Arrays.asList(cursor.getColumnNames()));
|
||||
do {
|
||||
Model entity = Cache.getEntity(type, cursor.getLong(columnsOrdered.indexOf(idName)));
|
||||
if (entity == null) {
|
||||
entity = (T) entityConstructor.newInstance();
|
||||
}
|
||||
|
||||
entity.loadFromCursor(cursor);
|
||||
entities.add((T) entity);
|
||||
}
|
||||
while (cursor.moveToNext());
|
||||
}
|
||||
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(
|
||||
"Your model " + type.getName() + " does not define a default " +
|
||||
"constructor. The default constructor is required for " +
|
||||
"now in ActiveAndroid models, as the process to " +
|
||||
"populate the ORM model is : " +
|
||||
"1. instantiate default model " +
|
||||
"2. populate fields"
|
||||
);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e("Failed to process cursor.", e);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static int processIntCursor(final Cursor cursor) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static List<String> lexSqlScript(String sqlScript) {
|
||||
ArrayList<String> sl = new ArrayList<String>();
|
||||
boolean inString = false, quoteNext = false;
|
||||
StringBuilder b = new StringBuilder(100);
|
||||
|
||||
for (int i = 0; i < sqlScript.length(); i++) {
|
||||
char c = sqlScript.charAt(i);
|
||||
|
||||
if (c == ';' && !inString && !quoteNext) {
|
||||
sl.add(b.toString());
|
||||
b = new StringBuilder(100);
|
||||
inString = false;
|
||||
quoteNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\'' && !quoteNext) {
|
||||
inString = !inString;
|
||||
}
|
||||
|
||||
quoteNext = c == '\\' && !quoteNext;
|
||||
|
||||
b.append(c);
|
||||
}
|
||||
|
||||
if (b.length() > 0) {
|
||||
sl.add(b.toString());
|
||||
}
|
||||
|
||||
return sl;
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/com/activeandroid/util/SqlParser.java
Normal file
110
app/src/main/java/com/activeandroid/util/SqlParser.java
Normal file
@@ -0,0 +1,110 @@
|
||||
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014 Markus Pfeiffer
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class SqlParser {
|
||||
|
||||
public final static int STATE_NONE = 0;
|
||||
public final static int STATE_STRING = 1;
|
||||
public final static int STATE_COMMENT = 2;
|
||||
public final static int STATE_COMMENT_BLOCK = 3;
|
||||
|
||||
public static List<String> parse(final InputStream stream) throws IOException {
|
||||
|
||||
final BufferedInputStream buffer = new BufferedInputStream(stream);
|
||||
final List<String> commands = new ArrayList<String>();
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
|
||||
try {
|
||||
final Tokenizer tokenizer = new Tokenizer(buffer);
|
||||
int state = STATE_NONE;
|
||||
|
||||
while (tokenizer.hasNext()) {
|
||||
final char c = (char) tokenizer.next();
|
||||
|
||||
if (state == STATE_COMMENT_BLOCK) {
|
||||
if (tokenizer.skip("*/")) {
|
||||
state = STATE_NONE;
|
||||
}
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_COMMENT) {
|
||||
if (isNewLine(c)) {
|
||||
state = STATE_NONE;
|
||||
}
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
|
||||
state = STATE_COMMENT_BLOCK;
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && tokenizer.skip("--")) {
|
||||
state = STATE_COMMENT;
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && c == ';') {
|
||||
final String command = sb.toString().trim();
|
||||
commands.add(command);
|
||||
sb.setLength(0);
|
||||
continue;
|
||||
|
||||
} else if (state == STATE_NONE && c == '\'') {
|
||||
state = STATE_STRING;
|
||||
|
||||
} else if (state == STATE_STRING && c == '\'') {
|
||||
state = STATE_NONE;
|
||||
|
||||
}
|
||||
|
||||
if (state == STATE_NONE || state == STATE_STRING) {
|
||||
if (state == STATE_NONE && isWhitespace(c)) {
|
||||
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
|
||||
sb.append(' ');
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
IOUtils.closeQuietly(buffer);
|
||||
}
|
||||
|
||||
if (sb.length() > 0) {
|
||||
commands.add(sb.toString().trim());
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static boolean isNewLine(final char c) {
|
||||
return c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
private static boolean isWhitespace(final char c) {
|
||||
return c == '\r' || c == '\n' || c == '\t' || c == ' ';
|
||||
}
|
||||
}
|
||||
76
app/src/main/java/com/activeandroid/util/Tokenizer.java
Normal file
76
app/src/main/java/com/activeandroid/util/Tokenizer.java
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
package com.activeandroid.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014 Markus Pfeiffer
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
public class Tokenizer {
|
||||
|
||||
private final InputStream mStream;
|
||||
|
||||
private boolean mIsNext;
|
||||
private int mCurrent;
|
||||
|
||||
public Tokenizer(final InputStream in) {
|
||||
this.mStream = in;
|
||||
}
|
||||
|
||||
public boolean hasNext() throws IOException {
|
||||
|
||||
if (!this.mIsNext) {
|
||||
this.mIsNext = true;
|
||||
this.mCurrent = this.mStream.read();
|
||||
}
|
||||
return this.mCurrent != -1;
|
||||
}
|
||||
|
||||
public int next() throws IOException {
|
||||
|
||||
if (!this.mIsNext) {
|
||||
this.mCurrent = this.mStream.read();
|
||||
}
|
||||
this.mIsNext = false;
|
||||
return this.mCurrent;
|
||||
}
|
||||
|
||||
public boolean skip(final String s) throws IOException {
|
||||
|
||||
if (s == null || s.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s.charAt(0) != this.mCurrent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int len = s.length();
|
||||
this.mStream.mark(len - 1);
|
||||
|
||||
for (int n = 1; n < len; n++) {
|
||||
final int value = this.mStream.read();
|
||||
|
||||
if (value != s.charAt(n)) {
|
||||
this.mStream.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
57
app/src/main/java/com/activeandroid/widget/ModelAdapter.java
Normal file
57
app/src/main/java/com/activeandroid/widget/ModelAdapter.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.activeandroid.widget;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
|
||||
public class ModelAdapter<T extends Model> extends ArrayAdapter<T> {
|
||||
public ModelAdapter(Context context, int textViewResourceId) {
|
||||
super(context, textViewResourceId);
|
||||
}
|
||||
|
||||
public ModelAdapter(Context context, int resource, int textViewResourceId) {
|
||||
super(context, resource, textViewResourceId);
|
||||
}
|
||||
|
||||
public ModelAdapter(Context context, int textViewResourceId, List<T> objects) {
|
||||
super(context, textViewResourceId, objects);
|
||||
}
|
||||
|
||||
public ModelAdapter(Context context, int resource, int textViewResourceId, List<T> objects) {
|
||||
super(context, resource, textViewResourceId, objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the adapter and, if data != null, fills if with new Items.
|
||||
*
|
||||
* @param collection A Collection<? extends T> which members get added to the adapter.
|
||||
*/
|
||||
public void setData(Collection<? extends T> collection) {
|
||||
clear();
|
||||
|
||||
if (collection != null) {
|
||||
for (T item : collection) {
|
||||
add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The Id of the record at position.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
T item = getItem(position);
|
||||
|
||||
if (item != null) {
|
||||
return item.getId();
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,10 @@ package org.isoron.uhabits;
|
||||
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.uhabits.models.sqlite.*;
|
||||
import org.isoron.uhabits.notifications.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
@@ -38,7 +38,7 @@ import java.io.*;
|
||||
*/
|
||||
public class HabitsApplication extends Application
|
||||
{
|
||||
private static Context context;
|
||||
private Context context;
|
||||
|
||||
private static AppComponent component;
|
||||
|
||||
@@ -58,26 +58,14 @@ public class HabitsApplication extends Application
|
||||
HabitsApplication.component = component;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public static Context getContext()
|
||||
{
|
||||
if (context == null) throw new RuntimeException("context is null");
|
||||
return context;
|
||||
}
|
||||
|
||||
public static boolean isTestMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
String testClass = "org.isoron.uhabits.BaseAndroidTest";
|
||||
context.getClassLoader().loadClass(testClass);
|
||||
}
|
||||
Class.forName ("org.isoron.uhabits.BaseAndroidTest");
|
||||
return true;
|
||||
}
|
||||
catch (final Exception e)
|
||||
catch (final ClassNotFoundException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -87,7 +75,7 @@ public class HabitsApplication extends Application
|
||||
public void onCreate()
|
||||
{
|
||||
super.onCreate();
|
||||
HabitsApplication.context = this;
|
||||
context = this;
|
||||
|
||||
component = DaggerAppComponent
|
||||
.builder()
|
||||
@@ -96,11 +84,20 @@ public class HabitsApplication extends Application
|
||||
|
||||
if (isTestMode())
|
||||
{
|
||||
File db = DatabaseUtils.getDatabaseFile();
|
||||
File db = DatabaseUtils.getDatabaseFile(context);
|
||||
if (db.exists()) db.delete();
|
||||
}
|
||||
|
||||
DatabaseUtils.initializeActiveAndroid();
|
||||
try
|
||||
{
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
}
|
||||
catch (InvalidDatabaseVersionException e)
|
||||
{
|
||||
File db = DatabaseUtils.getDatabaseFile(context);
|
||||
db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
}
|
||||
|
||||
widgetUpdater = component.getWidgetUpdater();
|
||||
widgetUpdater.startListening();
|
||||
@@ -125,7 +122,7 @@ public class HabitsApplication extends Application
|
||||
@Override
|
||||
public void onTerminate()
|
||||
{
|
||||
HabitsApplication.context = null;
|
||||
context = null;
|
||||
ActiveAndroid.dispose();
|
||||
|
||||
reminderScheduler.stopListening();
|
||||
|
||||
@@ -40,6 +40,7 @@ import java.io.*;
|
||||
|
||||
import static android.os.Build.VERSION.*;
|
||||
import static android.os.Build.VERSION_CODES.*;
|
||||
import static android.support.v4.content.FileProvider.*;
|
||||
|
||||
/**
|
||||
* Base class for all screens in the application.
|
||||
@@ -50,6 +51,8 @@ import static android.os.Build.VERSION_CODES.*;
|
||||
*/
|
||||
public class BaseScreen
|
||||
{
|
||||
public static final int REQUEST_CREATE_DOCUMENT = 1;
|
||||
|
||||
protected BaseActivity activity;
|
||||
|
||||
@Nullable
|
||||
@@ -230,11 +233,14 @@ public class BaseScreen
|
||||
|
||||
public void showSendFileScreen(@NonNull String archiveFilename)
|
||||
{
|
||||
File file = new File(archiveFilename);
|
||||
Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.setType("application/zip");
|
||||
intent.putExtra(Intent.EXTRA_STREAM,
|
||||
Uri.fromFile(new File(archiveFilename)));
|
||||
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public class BaseSystem
|
||||
|
||||
if (context == null) throw new RuntimeException(
|
||||
"application context should not be null");
|
||||
File dir = FileUtils.getFilesDir("Logs");
|
||||
File dir = FileUtils.getFilesDir(context, "Logs");
|
||||
if (dir == null) throw new IOException("log dir should not be null");
|
||||
|
||||
File logFile =
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.support.v7.widget.Toolbar;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.BuildConfig;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.intents.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
@@ -94,6 +94,13 @@ public class AboutRootView extends BaseRootView
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvTranslate)
|
||||
public void onClickTranslate()
|
||||
{
|
||||
Intent intent = intents.helpTranslate(getContext());
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvRate)
|
||||
public void onClickRate()
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.support.v7.app.*;
|
||||
|
||||
import com.google.auto.factory.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
@@ -78,6 +78,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
|
||||
{
|
||||
long id = savedInstanceState.getLong("habit", -1);
|
||||
if (id > 0) this.habit = habitList.getById(id);
|
||||
historyChart.onRestoreInstanceState(
|
||||
savedInstanceState.getParcelable("historyChart"));
|
||||
}
|
||||
|
||||
int padding =
|
||||
@@ -129,6 +131,7 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
|
||||
public void onSaveInstanceState(Bundle outState)
|
||||
{
|
||||
outState.putLong("habit", habit.getId());
|
||||
outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.os.*;
|
||||
import android.support.v4.os.*;
|
||||
|
||||
public class BundleSavedState extends android.support.v4.view.AbsSavedState
|
||||
{
|
||||
public static final Parcelable.Creator<BundleSavedState> CREATOR =
|
||||
ParcelableCompat.newCreator(
|
||||
new ParcelableCompatCreatorCallbacks<BundleSavedState>()
|
||||
{
|
||||
@Override
|
||||
public BundleSavedState createFromParcel(Parcel source,
|
||||
ClassLoader loader)
|
||||
{
|
||||
return new BundleSavedState(source, loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BundleSavedState[] newArray(int size)
|
||||
{
|
||||
return new BundleSavedState[size];
|
||||
}
|
||||
});
|
||||
|
||||
public final Bundle bundle;
|
||||
|
||||
public BundleSavedState(Parcelable superState, Bundle bundle)
|
||||
{
|
||||
super(superState);
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public BundleSavedState(Parcel source, ClassLoader loader)
|
||||
{
|
||||
super(source, loader);
|
||||
this.bundle = source.readBundle(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags)
|
||||
{
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeBundle(bundle);
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ public class FrequencyChart extends ScrollableChart
|
||||
float scale = 1.0f/maxFreq * value;
|
||||
float radius = maxRadius * scale;
|
||||
|
||||
int colorIndex = Math.round((colors.length-1) * scale);
|
||||
int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale));
|
||||
pGraph.setColor(colors[colorIndex]);
|
||||
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import org.isoron.uhabits.utils.*;
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.models.Checkmark.*;
|
||||
import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY;
|
||||
import static org.isoron.uhabits.models.Checkmark.UNCHECKED;
|
||||
|
||||
public class HistoryChart extends ScrollableChart
|
||||
{
|
||||
@@ -112,10 +113,21 @@ public class HistoryChart extends ScrollableChart
|
||||
if (!isEditable) return false;
|
||||
|
||||
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
float x, y;
|
||||
|
||||
try
|
||||
{
|
||||
int pointerId = e.getPointerId(0);
|
||||
float x = e.getX(pointerId);
|
||||
float y = e.getY(pointerId);
|
||||
x = e.getX(pointerId);
|
||||
y = e.getY(pointerId);
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false;
|
||||
}
|
||||
|
||||
final Long timestamp = positionToTimestamp(x, y);
|
||||
if (timestamp == null) return false;
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.animation.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
@@ -32,7 +33,9 @@ public abstract class ScrollableChart extends View
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
private int scrollerBucketSize;
|
||||
private int scrollerBucketSize = 1;
|
||||
|
||||
private int direction = 1;
|
||||
|
||||
private GestureDetector detector;
|
||||
|
||||
@@ -40,6 +43,10 @@ public abstract class ScrollableChart extends View
|
||||
|
||||
private ValueAnimator scrollAnimator;
|
||||
|
||||
private ScrollController scrollController;
|
||||
|
||||
private int maxDataOffset = 10000;
|
||||
|
||||
public ScrollableChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
@@ -63,8 +70,7 @@ public abstract class ScrollableChart extends View
|
||||
if (!scroller.isFinished())
|
||||
{
|
||||
scroller.computeScrollOffset();
|
||||
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
|
||||
postInvalidate();
|
||||
updateDataOffset();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -85,19 +91,50 @@ public abstract class ScrollableChart extends View
|
||||
float velocityY)
|
||||
{
|
||||
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
|
||||
(int) velocityX / 2, 0, 0, 100000, 0, 0);
|
||||
direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
|
||||
invalidate();
|
||||
|
||||
scrollAnimator.setDuration(scroller.getDuration());
|
||||
scrollAnimator.start();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
private int getMaxX()
|
||||
{
|
||||
return maxDataOffset * scrollerBucketSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
if(!(state instanceof BundleSavedState))
|
||||
{
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
int x = bss.bundle.getInt("x");
|
||||
int y = bss.bundle.getInt("y");
|
||||
direction = bss.bundle.getInt("direction");
|
||||
dataOffset = bss.bundle.getInt("dataOffset");
|
||||
maxDataOffset = bss.bundle.getInt("maxDataOffset");
|
||||
scroller.startScroll(0, 0, x, y, 0);
|
||||
scroller.computeScrollOffset();
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("x", scroller.getCurrX());
|
||||
bundle.putInt("y", scroller.getCurrY());
|
||||
bundle.putInt("dataOffset", dataOffset);
|
||||
bundle.putInt("direction", direction);
|
||||
bundle.putInt("maxDataOffset", maxDataOffset);
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,12 +148,14 @@ public abstract class ScrollableChart extends View
|
||||
if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
|
||||
}
|
||||
|
||||
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(),
|
||||
(int) -dx, (int) dy, 0);
|
||||
scroller.computeScrollOffset();
|
||||
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
|
||||
postInvalidate();
|
||||
|
||||
dx = - direction * dx;
|
||||
dx = Math.min(dx, getMaxX() - scroller.getCurrX());
|
||||
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx,
|
||||
(int) dy, 0);
|
||||
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -138,6 +177,32 @@ public abstract class ScrollableChart extends View
|
||||
return detector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setDirection(int direction)
|
||||
{
|
||||
if (direction != 1 && direction != -1)
|
||||
throw new IllegalArgumentException();
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void setMaxDataOffset(int maxDataOffset)
|
||||
{
|
||||
this.maxDataOffset = maxDataOffset;
|
||||
this.dataOffset = Math.min(dataOffset, maxDataOffset);
|
||||
scrollController.onDataOffsetChanged(this.dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setScrollController(ScrollController scrollController)
|
||||
{
|
||||
this.scrollController = scrollController;
|
||||
}
|
||||
|
||||
public void setScrollerBucketSize(int scrollerBucketSize)
|
||||
{
|
||||
this.scrollerBucketSize = scrollerBucketSize;
|
||||
@@ -149,5 +214,25 @@ public abstract class ScrollableChart extends View
|
||||
scroller = new Scroller(context, null, true);
|
||||
scrollAnimator = ValueAnimator.ofFloat(0, 1);
|
||||
scrollAnimator.addUpdateListener(this);
|
||||
scrollController = new ScrollController() {};
|
||||
}
|
||||
|
||||
private void updateDataOffset()
|
||||
{
|
||||
int newDataOffset = scroller.getCurrX() / scrollerBucketSize;
|
||||
newDataOffset = Math.max(0, newDataOffset);
|
||||
newDataOffset = Math.min(maxDataOffset, newDataOffset);
|
||||
|
||||
if (newDataOffset != dataOffset)
|
||||
{
|
||||
dataOffset = newDataOffset;
|
||||
scrollController.onDataOffsetChanged(dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ScrollController
|
||||
{
|
||||
default void onDataOffsetChanged(int newDataOffset) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.view.*;
|
||||
import com.android.datetimepicker.time.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.common.dialogs.*;
|
||||
import org.isoron.uhabits.commands.*;
|
||||
@@ -38,6 +39,8 @@ import java.util.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
import static org.isoron.uhabits.activities.ThemeSwitcher.*;
|
||||
|
||||
public abstract class BaseDialog extends AppCompatDialogFragment
|
||||
{
|
||||
@Nullable
|
||||
@@ -61,6 +64,18 @@ public abstract class BaseDialog extends AppCompatDialogFragment
|
||||
|
||||
private ColorPickerDialogFactory colorPickerDialogFactory;
|
||||
|
||||
@Override
|
||||
public int getTheme()
|
||||
{
|
||||
AppComponent component =
|
||||
((HabitsApplication) getContext().getApplicationContext()).getComponent();
|
||||
|
||||
if(component.getPreferences().getTheme() == THEME_LIGHT)
|
||||
return R.style.DialogWithTitle;
|
||||
else
|
||||
return R.style.DarkDialogWithTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.support.v4.app.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.habits.list.model.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
/**
|
||||
* Activity that allows the user to see and modify the list of habits.
|
||||
@@ -43,6 +44,8 @@ public class ListHabitsActivity extends BaseActivity
|
||||
|
||||
private Preferences prefs;
|
||||
|
||||
private MidnightTimer midnightTimer;
|
||||
|
||||
public ListHabitsComponent getListHabitsComponent()
|
||||
{
|
||||
return component;
|
||||
@@ -77,6 +80,8 @@ public class ListHabitsActivity extends BaseActivity
|
||||
screen.setSelectionMenu(selectionMenu);
|
||||
rootView.setController(controller, selectionMenu);
|
||||
|
||||
midnightTimer = component.getMidnightTimer();
|
||||
|
||||
setScreen(screen);
|
||||
controller.onStartup();
|
||||
}
|
||||
@@ -84,6 +89,7 @@ public class ListHabitsActivity extends BaseActivity
|
||||
@Override
|
||||
protected void onPause()
|
||||
{
|
||||
midnightTimer.onPause();
|
||||
screen.onDettached();
|
||||
adapter.cancelRefresh();
|
||||
super.onPause();
|
||||
@@ -95,6 +101,7 @@ public class ListHabitsActivity extends BaseActivity
|
||||
adapter.refresh();
|
||||
screen.onAttached();
|
||||
rootView.postInvalidate();
|
||||
midnightTimer.onResume();
|
||||
|
||||
if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
|
||||
prefs.isPureBlackEnabled() != pureBlack)
|
||||
|
||||
@@ -23,13 +23,14 @@ import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.habits.list.controllers.*;
|
||||
import org.isoron.uhabits.activities.habits.list.model.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@ActivityScope
|
||||
@Component(modules = { ActivityModule.class },
|
||||
dependencies = { AppComponent.class })
|
||||
public interface ListHabitsComponent extends ActivityComponent
|
||||
public interface ListHabitsComponent
|
||||
{
|
||||
CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory();
|
||||
|
||||
@@ -44,4 +45,6 @@ public interface ListHabitsComponent extends ActivityComponent
|
||||
ListHabitsScreen getScreen();
|
||||
|
||||
ListHabitsSelectionMenu getSelectionMenu();
|
||||
|
||||
MidnightTimer getMidnightTimer();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import javax.inject.*;
|
||||
public class ListHabitsController
|
||||
implements HabitCardListController.HabitListener
|
||||
{
|
||||
|
||||
@NonNull
|
||||
private final ListHabitsScreen screen;
|
||||
|
||||
@@ -70,6 +71,8 @@ public class ListHabitsController
|
||||
|
||||
private ExportCSVTaskFactory exportCSVFactory;
|
||||
|
||||
private ExportDBTaskFactory exportDBFactory;
|
||||
|
||||
@Inject
|
||||
public ListHabitsController(@NonNull BaseSystem system,
|
||||
@NonNull CommandRunner commandRunner,
|
||||
@@ -82,7 +85,8 @@ public class ListHabitsController
|
||||
@NonNull WidgetUpdater widgetUpdater,
|
||||
@NonNull
|
||||
ImportDataTaskFactory importTaskFactory,
|
||||
@NonNull ExportCSVTaskFactory exportCSVFactory)
|
||||
@NonNull ExportCSVTaskFactory exportCSVFactory,
|
||||
@NonNull ExportDBTaskFactory exportDBFactory)
|
||||
{
|
||||
this.adapter = adapter;
|
||||
this.commandRunner = commandRunner;
|
||||
@@ -95,6 +99,7 @@ public class ListHabitsController
|
||||
this.widgetUpdater = widgetUpdater;
|
||||
this.importTaskFactory = importTaskFactory;
|
||||
this.exportCSVFactory = exportCSVFactory;
|
||||
this.exportDBFactory = exportDBFactory;
|
||||
}
|
||||
|
||||
public void onExportCSV()
|
||||
@@ -110,7 +115,7 @@ public class ListHabitsController
|
||||
|
||||
public void onExportDB()
|
||||
{
|
||||
taskRunner.execute(new ExportDBTask(filename -> {
|
||||
taskRunner.execute(exportDBFactory.create(filename -> {
|
||||
if (filename != null) screen.showSendFileScreen(filename);
|
||||
else screen.showMessage(R.string.could_not_export);
|
||||
}));
|
||||
@@ -128,7 +133,8 @@ public class ListHabitsController
|
||||
taskRunner.execute(() -> habitList.reorder(from, to));
|
||||
}
|
||||
|
||||
public void onImportData(@NonNull File file)
|
||||
public void onImportData(@NonNull File file,
|
||||
@NonNull OnFinishedListener finishedListener)
|
||||
{
|
||||
taskRunner.execute(importTaskFactory.create(file, result -> {
|
||||
switch (result)
|
||||
@@ -146,6 +152,8 @@ public class ListHabitsController
|
||||
screen.showMessage(R.string.could_not_import);
|
||||
break;
|
||||
}
|
||||
|
||||
finishedListener.onFinish();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -208,4 +216,9 @@ public class ListHabitsController
|
||||
prefs.updateLastHint(-1, DateUtils.getStartOfToday());
|
||||
screen.showIntroScreen();
|
||||
}
|
||||
|
||||
public interface OnFinishedListener
|
||||
{
|
||||
void onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,22 @@ public class ListHabitsMenu extends BaseMenu
|
||||
invalidate();
|
||||
return true;
|
||||
|
||||
case R.id.actionSortColor:
|
||||
adapter.setOrder(HabitList.Order.BY_COLOR);
|
||||
return true;
|
||||
|
||||
case R.id.actionSortManual:
|
||||
adapter.setOrder(HabitList.Order.BY_POSITION);
|
||||
return true;
|
||||
|
||||
case R.id.actionSortName:
|
||||
adapter.setOrder(HabitList.Order.BY_NAME);
|
||||
return true;
|
||||
|
||||
case R.id.actionSortScore:
|
||||
adapter.setOrder(HabitList.Order.BY_SCORE);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import android.support.v7.widget.Toolbar;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.activities.habits.list.controllers.*;
|
||||
import org.isoron.uhabits.activities.habits.list.model.*;
|
||||
import org.isoron.uhabits.activities.habits.list.views.*;
|
||||
@@ -43,7 +44,7 @@ import butterknife.*;
|
||||
public class ListHabitsRootView extends BaseRootView
|
||||
implements ModelObservable.Listener, TaskRunner.Listener
|
||||
{
|
||||
public static final int MAX_CHECKMARK_COUNT = 21;
|
||||
public static final int MAX_CHECKMARK_COUNT = 60;
|
||||
|
||||
@BindView(R.id.listView)
|
||||
HabitCardListView listView;
|
||||
@@ -132,6 +133,13 @@ public class ListHabitsRootView extends BaseRootView
|
||||
listController.setSelectionListener(menu);
|
||||
listView.setController(listController);
|
||||
menu.setListController(listController);
|
||||
header.setScrollController(new ScrollableChart.ScrollController() {
|
||||
@Override
|
||||
public void onDataOffsetChanged(int newDataOffset)
|
||||
{
|
||||
listView.setDataOffset(newDataOffset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,6 +164,7 @@ public class ListHabitsRootView extends BaseRootView
|
||||
{
|
||||
int count = getCheckmarkCount();
|
||||
header.setButtonCount(count);
|
||||
header.setMaxDataOffset(Math.max(MAX_CHECKMARK_COUNT - count, 0));
|
||||
listView.setCheckmarkCount(count);
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list;
|
||||
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.net.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
@@ -31,24 +33,32 @@ import org.isoron.uhabits.commands.*;
|
||||
import org.isoron.uhabits.intents.*;
|
||||
import org.isoron.uhabits.io.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import static android.os.Build.VERSION.*;
|
||||
import static android.os.Build.VERSION_CODES.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ListHabitsScreen extends BaseScreen
|
||||
implements CommandRunner.Listener
|
||||
{
|
||||
public static final int RESULT_BUG_REPORT = 4;
|
||||
public static final int RESULT_IMPORT_DATA = 1;
|
||||
|
||||
public static final int RESULT_EXPORT_CSV = 2;
|
||||
|
||||
public static final int RESULT_EXPORT_DB = 3;
|
||||
|
||||
public static final int RESULT_BUG_REPORT = 4;
|
||||
|
||||
public static final int RESULT_REPAIR_DB = 5;
|
||||
|
||||
public static final int RESULT_IMPORT_DATA = 1;
|
||||
public static final int REQUEST_OPEN_DOCUMENT = 6;
|
||||
|
||||
public static final int REQUEST_SETTINGS = 7;
|
||||
|
||||
@Nullable
|
||||
private ListHabitsController controller;
|
||||
@@ -125,6 +135,15 @@ public class ListHabitsScreen extends BaseScreen
|
||||
|
||||
@Override
|
||||
public void onResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
if (requestCode == REQUEST_OPEN_DOCUMENT)
|
||||
onOpenDocumentResult(resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_SETTINGS)
|
||||
onSettingsResult(resultCode);
|
||||
}
|
||||
|
||||
private void onSettingsResult(int resultCode)
|
||||
{
|
||||
if (controller == null) return;
|
||||
|
||||
@@ -152,6 +171,30 @@ public class ListHabitsScreen extends BaseScreen
|
||||
}
|
||||
}
|
||||
|
||||
private void onOpenDocumentResult(int resultCode, Intent data)
|
||||
{
|
||||
if (controller == null) return;
|
||||
if (resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
try
|
||||
{
|
||||
Uri uri = data.getData();
|
||||
ContentResolver cr = activity.getContentResolver();
|
||||
InputStream is = cr.openInputStream(uri);
|
||||
|
||||
File cacheDir = activity.getExternalCacheDir();
|
||||
File tempFile = File.createTempFile("import", "", cacheDir);
|
||||
|
||||
FileUtils.copy(is, tempFile);
|
||||
controller.onImportData(tempFile, () -> tempFile.delete());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
showMessage(R.string.could_not_import);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setController(@Nullable ListHabitsController controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
@@ -208,6 +251,21 @@ public class ListHabitsScreen extends BaseScreen
|
||||
}
|
||||
|
||||
public void showImportScreen()
|
||||
{
|
||||
if (SDK_INT < KITKAT)
|
||||
{
|
||||
showImportScreenPreKitKat();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
|
||||
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
|
||||
}
|
||||
|
||||
public void showImportScreenPreKitKat()
|
||||
{
|
||||
File dir = dirFinder.findStorageDir(null);
|
||||
|
||||
@@ -220,7 +278,8 @@ public class ListHabitsScreen extends BaseScreen
|
||||
FilePickerDialog picker = filePickerDialogFactory.create(dir);
|
||||
|
||||
if (controller != null)
|
||||
picker.setListener(file -> controller.onImportData(file));
|
||||
picker.setListener(file -> controller.onImportData(file, () -> {}));
|
||||
|
||||
activity.showDialog(picker.getDialog());
|
||||
}
|
||||
|
||||
@@ -233,7 +292,7 @@ public class ListHabitsScreen extends BaseScreen
|
||||
public void showSettingsScreen()
|
||||
{
|
||||
Intent intent = intentFactory.startSettingsActivity(activity);
|
||||
activity.startActivityForResult(intent, 0);
|
||||
activity.startActivityForResult(intent, REQUEST_SETTINGS);
|
||||
}
|
||||
|
||||
public void toggleNightMode()
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.habits.list.*;
|
||||
import org.isoron.uhabits.activities.habits.list.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -41,7 +43,7 @@ import javax.inject.*;
|
||||
@ActivityScope
|
||||
public class HabitCardListAdapter
|
||||
extends RecyclerView.Adapter<HabitCardViewHolder>
|
||||
implements HabitCardListCache.Listener
|
||||
implements HabitCardListCache.Listener, MidnightTimer.MidnightListener
|
||||
{
|
||||
@NonNull
|
||||
private ModelObservable observable;
|
||||
@@ -55,19 +57,36 @@ public class HabitCardListAdapter
|
||||
@NonNull
|
||||
private final HabitCardListCache cache;
|
||||
|
||||
@NonNull
|
||||
private Preferences preferences;
|
||||
|
||||
private final MidnightTimer midnightTimer;
|
||||
|
||||
@Inject
|
||||
public HabitCardListAdapter(@NonNull HabitCardListCache cache)
|
||||
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
|
||||
@NonNull Preferences preferences,
|
||||
@NonNull MidnightTimer midnightTimer)
|
||||
{
|
||||
this.preferences = preferences;
|
||||
this.selected = new LinkedList<>();
|
||||
this.observable = new ModelObservable();
|
||||
this.cache = cache;
|
||||
|
||||
this.midnightTimer = midnightTimer;
|
||||
|
||||
cache.setListener(this);
|
||||
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
|
||||
cache.setOrder(preferences.getDefaultOrder());
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atMidnight()
|
||||
{
|
||||
cache.refreshAllHabits();
|
||||
}
|
||||
|
||||
public void cancelRefresh()
|
||||
{
|
||||
cache.cancelTasks();
|
||||
@@ -86,11 +105,10 @@ public class HabitCardListAdapter
|
||||
* Returns the item that occupies a certain position on the list
|
||||
*
|
||||
* @param position position of the item
|
||||
* @return the item at given position
|
||||
* @throws IndexOutOfBoundsException if position is not valid
|
||||
* @return the item at given position or null if position is invalid
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNull
|
||||
@Nullable
|
||||
public Habit getItem(int position)
|
||||
{
|
||||
return cache.getHabitByPosition(position);
|
||||
@@ -130,12 +148,18 @@ public class HabitCardListAdapter
|
||||
return selected.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isSortable()
|
||||
{
|
||||
return cache.getOrder() == HabitList.Order.BY_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter that it has been attached to a ListView.
|
||||
*/
|
||||
public void onAttached()
|
||||
{
|
||||
cache.onAttached();
|
||||
midnightTimer.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,6 +177,20 @@ public class HabitCardListAdapter
|
||||
listView.bindCardView(holder, habit, score, checkmarks, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.attachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.detachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType)
|
||||
@@ -168,6 +206,7 @@ public class HabitCardListAdapter
|
||||
public void onDetached()
|
||||
{
|
||||
cache.onDetached();
|
||||
midnightTimer.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,6 +299,12 @@ public class HabitCardListAdapter
|
||||
this.listView = listView;
|
||||
}
|
||||
|
||||
public void setOrder(HabitList.Order order)
|
||||
{
|
||||
cache.setOrder(order);
|
||||
preferences.setDefaultOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects or deselects the item at a given position.
|
||||
*
|
||||
@@ -268,6 +313,8 @@ public class HabitCardListAdapter
|
||||
public void toggleSelection(int position)
|
||||
{
|
||||
Habit h = getItem(position);
|
||||
if (h == null) return;
|
||||
|
||||
int k = selected.indexOf(h);
|
||||
if (k < 0) selected.add(h);
|
||||
else selected.remove(h);
|
||||
|
||||
@@ -93,12 +93,12 @@ public class HabitCardListCache implements CommandRunner.Listener
|
||||
* Returns the habits that occupies a certain position on the list.
|
||||
*
|
||||
* @param position the position of the habit
|
||||
* @return the habit at given position
|
||||
* @throws IndexOutOfBoundsException if position is not valid
|
||||
* @return the habit at given position or null if position is invalid
|
||||
*/
|
||||
@NonNull
|
||||
public Habit getHabitByPosition(int position)
|
||||
@Nullable
|
||||
public synchronized Habit getHabitByPosition(int position)
|
||||
{
|
||||
if(position < 0 || position >= data.habits.size()) return null;
|
||||
return data.habits.get(position);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@ public class HabitCardListCache implements CommandRunner.Listener
|
||||
return data.habits.size();
|
||||
}
|
||||
|
||||
public HabitList.Order getOrder()
|
||||
{
|
||||
return filteredHabits.getOrder();
|
||||
}
|
||||
|
||||
public int getScore(long habitId)
|
||||
{
|
||||
return data.scores.get(habitId);
|
||||
@@ -180,6 +185,13 @@ public class HabitCardListCache implements CommandRunner.Listener
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setOrder(HabitList.Order order)
|
||||
{
|
||||
allHabits.setOrder(order);
|
||||
filteredHabits.setOrder(order);
|
||||
refreshAllHabits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the data on the
|
||||
* cache has been modified.
|
||||
|
||||
@@ -20,21 +20,34 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.text.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.habits.list.controllers.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
public class CheckmarkButtonView extends TextView
|
||||
import static android.view.View.MeasureSpec.*;
|
||||
import static org.isoron.uhabits.models.Checkmark.*;
|
||||
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
|
||||
|
||||
public class CheckmarkButtonView extends View
|
||||
{
|
||||
private int color;
|
||||
|
||||
private int value;
|
||||
|
||||
private StyledResources res;
|
||||
private StyledResources styledRes;
|
||||
|
||||
private TextPaint paint;
|
||||
|
||||
private int lowContrastColor;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
public CheckmarkButtonView(Context context)
|
||||
{
|
||||
@@ -42,6 +55,21 @@ public class CheckmarkButtonView extends TextView
|
||||
init();
|
||||
}
|
||||
|
||||
public CheckmarkButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
|
||||
{
|
||||
super(ctx, attrs);
|
||||
init();
|
||||
|
||||
if(ctx == null) throw new IllegalStateException();
|
||||
if(attrs == null) throw new IllegalStateException();
|
||||
|
||||
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
|
||||
setColor(ColorUtils.getAndroidTestColor(paletteColor));
|
||||
|
||||
int value = getIntAttribute(ctx, attrs, "value", 0);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.color = color;
|
||||
@@ -57,55 +85,60 @@ public class CheckmarkButtonView extends TextView
|
||||
public void setValue(int value)
|
||||
{
|
||||
this.value = value;
|
||||
updateText();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void toggle()
|
||||
{
|
||||
value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED :
|
||||
Checkmark.CHECKED_EXPLICITLY);
|
||||
|
||||
value = (value == CHECKED_EXPLICITLY ? UNCHECKED : CHECKED_EXPLICITLY);
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
updateText();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Resources resources = getResources();
|
||||
|
||||
paint.setColor(value == CHECKED_EXPLICITLY ? color : lowContrastColor);
|
||||
int id = (value == UNCHECKED ? R.string.fa_times : R.string.fa_check);
|
||||
String label = resources.getString(id);
|
||||
float em = paint.measureText("m");
|
||||
|
||||
rect.set(0, 0, getWidth(), getHeight());
|
||||
rect.offset(0, 0.4f * em);
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
Resources res = getResources();
|
||||
int height = res.getDimensionPixelSize(R.dimen.checkmarkHeight);
|
||||
int width = res.getDimensionPixelSize(R.dimen.checkmarkWidth);
|
||||
|
||||
widthMeasureSpec = makeMeasureSpec(width, EXACTLY);
|
||||
heightMeasureSpec = makeMeasureSpec(height, EXACTLY);
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
res = new StyledResources(getContext());
|
||||
|
||||
setWillNotDraw(false);
|
||||
setHapticFeedbackEnabled(false);
|
||||
|
||||
setMinHeight(
|
||||
getResources().getDimensionPixelSize(R.dimen.checkmarkHeight));
|
||||
setMinWidth(
|
||||
getResources().getDimensionPixelSize(R.dimen.checkmarkWidth));
|
||||
|
||||
setFocusable(false);
|
||||
setGravity(Gravity.CENTER);
|
||||
setTypeface(InterfaceUtils.getFontAwesome(getContext()));
|
||||
}
|
||||
|
||||
private void updateText()
|
||||
{
|
||||
int lowContrastColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
Resources res = getResources();
|
||||
styledRes = new StyledResources(getContext());
|
||||
|
||||
if (value == Checkmark.CHECKED_EXPLICITLY)
|
||||
{
|
||||
setText(R.string.fa_check);
|
||||
setTextColor(color);
|
||||
}
|
||||
paint = new TextPaint();
|
||||
paint.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
|
||||
paint.setAntiAlias(true);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTextSize(res.getDimension(R.dimen.regularTextSize));
|
||||
|
||||
if (value == Checkmark.CHECKED_IMPLICITLY)
|
||||
{
|
||||
setText(R.string.fa_check);
|
||||
setTextColor(lowContrastColor);
|
||||
}
|
||||
|
||||
if (value == Checkmark.UNCHECKED)
|
||||
{
|
||||
setText(R.string.fa_times);
|
||||
setTextColor(lowContrastColor);
|
||||
}
|
||||
rect = new RectF();
|
||||
color = ColorUtils.getAndroidTestColor(0);
|
||||
lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
|
||||
@NonNull
|
||||
private Habit habit;
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
public CheckmarkPanelView(Context context)
|
||||
{
|
||||
super(context);
|
||||
@@ -75,19 +77,23 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
|
||||
return (CheckmarkButtonView) getChildAt(position);
|
||||
}
|
||||
|
||||
public void setCheckmarkValues(int[] checkmarkValues)
|
||||
public void setButtonCount(int newButtonCount)
|
||||
{
|
||||
this.checkmarkValues = checkmarkValues;
|
||||
|
||||
if (this.nButtons != checkmarkValues.length)
|
||||
if(nButtons != newButtonCount)
|
||||
{
|
||||
this.nButtons = checkmarkValues.length;
|
||||
nButtons = newButtonCount;
|
||||
addCheckmarkButtons();
|
||||
}
|
||||
|
||||
setupCheckmarkButtons();
|
||||
}
|
||||
|
||||
public void setCheckmarkValues(int[] checkmarkValues)
|
||||
{
|
||||
this.checkmarkValues = checkmarkValues;
|
||||
setupCheckmarkButtons();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.color = color;
|
||||
@@ -100,6 +106,12 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
|
||||
setupCheckmarkButtons();
|
||||
}
|
||||
|
||||
public void setDataOffset(int dataOffset)
|
||||
{
|
||||
this.dataOffset = dataOffset;
|
||||
setupCheckmarkButtons();
|
||||
}
|
||||
|
||||
public void setHabit(@NonNull Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
@@ -170,11 +182,13 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List
|
||||
{
|
||||
long timestamp = DateUtils.getStartOfToday();
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
timestamp -= day * dataOffset;
|
||||
|
||||
for (int i = 0; i < nButtons; i++)
|
||||
{
|
||||
CheckmarkButtonView buttonView = indexToButton(i);
|
||||
buttonView.setValue(checkmarkValues[i]);
|
||||
if(i + dataOffset >= checkmarkValues.length) break;
|
||||
buttonView.setValue(checkmarkValues[i + dataOffset]);
|
||||
buttonView.setColor(color);
|
||||
setupButtonControllers(timestamp, buttonView);
|
||||
timestamp -= day;
|
||||
|
||||
@@ -20,15 +20,17 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.widget.*;
|
||||
import android.support.v7.widget.helper.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.activities.habits.list.controllers.*;
|
||||
import org.isoron.uhabits.activities.habits.list.model.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -44,6 +46,10 @@ public class HabitCardListView extends RecyclerView
|
||||
|
||||
private int checkmarkCount;
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
private LinkedList<HabitCardViewHolder> attachedHolders;
|
||||
|
||||
public HabitCardListView(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
@@ -54,6 +60,13 @@ public class HabitCardListView extends RecyclerView
|
||||
TouchHelperCallback callback = new TouchHelperCallback();
|
||||
touchHelper = new ItemTouchHelper(callback);
|
||||
touchHelper.attachToRecyclerView(this);
|
||||
|
||||
attachedHolders = new LinkedList<>();
|
||||
}
|
||||
|
||||
public void attachCardView(HabitCardViewHolder holder)
|
||||
{
|
||||
attachedHolders.add(holder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,13 +88,12 @@ public class HabitCardListView extends RecyclerView
|
||||
int[] checkmarks,
|
||||
boolean selected)
|
||||
{
|
||||
int visibleCheckmarks[] =
|
||||
Arrays.copyOfRange(checkmarks, 0, checkmarkCount);
|
||||
|
||||
HabitCardView cardView = (HabitCardView) holder.itemView;
|
||||
cardView.setHabit(habit);
|
||||
cardView.setSelected(selected);
|
||||
cardView.setCheckmarkValues(visibleCheckmarks);
|
||||
cardView.setCheckmarkValues(checkmarks);
|
||||
cardView.setCheckmarkCount(checkmarkCount);
|
||||
cardView.setDataOffset(dataOffset);
|
||||
cardView.setScore(score);
|
||||
if (controller != null) setupCardViewController(holder);
|
||||
return cardView;
|
||||
@@ -92,6 +104,11 @@ public class HabitCardListView extends RecyclerView
|
||||
return new HabitCardView(getContext());
|
||||
}
|
||||
|
||||
public void detachCardView(HabitCardViewHolder holder)
|
||||
{
|
||||
attachedHolders.remove(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAdapter(RecyclerView.Adapter adapter)
|
||||
{
|
||||
@@ -109,6 +126,16 @@ public class HabitCardListView extends RecyclerView
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public void setDataOffset(int dataOffset)
|
||||
{
|
||||
this.dataOffset = dataOffset;
|
||||
for (HabitCardViewHolder holder : attachedHolders)
|
||||
{
|
||||
HabitCardView cardView = (HabitCardView) holder.itemView;
|
||||
cardView.setDataOffset(dataOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow()
|
||||
{
|
||||
@@ -123,6 +150,29 @@ public class HabitCardListView extends RecyclerView
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
if(!(state instanceof BundleSavedState))
|
||||
{
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
dataOffset = bss.bundle.getInt("dataOffset");
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("dataOffset", dataOffset);
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
protected void setupCardViewController(@NonNull HabitCardViewHolder holder)
|
||||
{
|
||||
HabitCardView cardView = (HabitCardView) holder.itemView;
|
||||
@@ -168,7 +218,7 @@ public class HabitCardListView extends RecyclerView
|
||||
{
|
||||
int position = holder.getAdapterPosition();
|
||||
if (controller != null) controller.onItemLongClick(position);
|
||||
touchHelper.startDrag(holder);
|
||||
if (adapter.isSortable()) touchHelper.startDrag(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
@@ -72,6 +72,8 @@ public class HabitCardView extends FrameLayout
|
||||
@Nullable
|
||||
private Habit habit;
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
public HabitCardView(Context context)
|
||||
{
|
||||
super(context);
|
||||
@@ -90,6 +92,11 @@ public class HabitCardView extends FrameLayout
|
||||
new Handler(Looper.getMainLooper()).post(() -> refresh());
|
||||
}
|
||||
|
||||
public void setCheckmarkCount(int checkmarkCount)
|
||||
{
|
||||
checkmarkPanel.setButtonCount(checkmarkCount);
|
||||
}
|
||||
|
||||
public void setCheckmarkValues(int checkmarks[])
|
||||
{
|
||||
checkmarkPanel.setCheckmarkValues(checkmarks);
|
||||
@@ -103,6 +110,12 @@ public class HabitCardView extends FrameLayout
|
||||
checkmarkPanel.setController(controller);
|
||||
}
|
||||
|
||||
public void setDataOffset(int dataOffset)
|
||||
{
|
||||
this.dataOffset = dataOffset;
|
||||
checkmarkPanel.setDataOffset(dataOffset);
|
||||
}
|
||||
|
||||
public void setHabit(@NonNull Habit habit)
|
||||
{
|
||||
if (this.habit != null) detachFromHabit();
|
||||
@@ -130,12 +143,13 @@ public class HabitCardView extends FrameLayout
|
||||
updateBackground(isSelected);
|
||||
}
|
||||
|
||||
public void triggerRipple(long timestamp)
|
||||
public synchronized void triggerRipple(long timestamp)
|
||||
{
|
||||
long today = DateUtils.getStartOfToday();
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
int offset = (int) ((today - timestamp) / day);
|
||||
int offset = (int) ((today - timestamp) / day) - dataOffset;
|
||||
CheckmarkButtonView button = checkmarkPanel.indexToButton(offset);
|
||||
if (button == null) return;
|
||||
|
||||
float y = button.getHeight() / 2.0f;
|
||||
float x = checkmarkPanel.getX() + button.getX() + button.getWidth() / 2;
|
||||
@@ -201,6 +215,7 @@ public class HabitCardView extends FrameLayout
|
||||
scoreRing.setPercentage(rand.nextFloat());
|
||||
checkmarkPanel.setColor(color);
|
||||
checkmarkPanel.setCheckmarkValues(values);
|
||||
checkmarkPanel.setButtonCount(5);
|
||||
}
|
||||
|
||||
private void refresh()
|
||||
|
||||
@@ -20,30 +20,39 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.text.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.activities.habits.list.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class HeaderView extends LinearLayout implements Preferences.Listener
|
||||
public class HeaderView extends ScrollableChart
|
||||
implements Preferences.Listener, MidnightTimer.MidnightListener
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
private int buttonCount;
|
||||
|
||||
@Nullable
|
||||
private Preferences prefs;
|
||||
|
||||
@Nullable
|
||||
private MidnightTimer midnightTimer;
|
||||
|
||||
private final TextPaint paint;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
public HeaderView(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
this.context = context;
|
||||
|
||||
if (isInEditMode())
|
||||
{
|
||||
@@ -56,51 +65,116 @@ public class HeaderView extends LinearLayout implements Preferences.Listener
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
prefs = app.getComponent().getPreferences();
|
||||
}
|
||||
|
||||
if (context instanceof ListHabitsActivity)
|
||||
{
|
||||
ListHabitsActivity activity = (ListHabitsActivity) context;
|
||||
midnightTimer = activity.getListHabitsComponent().getMidnightTimer();
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
setScrollerBucketSize((int) res.getDimension(R.dimen.checkmarkWidth));
|
||||
|
||||
StyledResources sr = new StyledResources(context);
|
||||
paint = new TextPaint();
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setTextSize(getResources().getDimension(R.dimen.tinyTextSize));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
paint.setColor(sr.getColor(R.attr.mediumContrastTextColor));
|
||||
|
||||
rect = new RectF();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atMidnight()
|
||||
{
|
||||
post(() -> invalidate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckmarkOrderChanged()
|
||||
{
|
||||
createButtons();
|
||||
updateDirection();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setButtonCount(int buttonCount)
|
||||
{
|
||||
this.buttonCount = buttonCount;
|
||||
createButtons();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow()
|
||||
{
|
||||
updateDirection();
|
||||
super.onAttachedToWindow();
|
||||
if (prefs != null) prefs.addListener(this);
|
||||
if (midnightTimer != null) midnightTimer.addListener(this);
|
||||
}
|
||||
|
||||
private void updateDirection()
|
||||
{
|
||||
int direction = -1;
|
||||
if (shouldReverseCheckmarks()) direction *= -1;
|
||||
if (InterfaceUtils.isLayoutRtl(this)) direction *= -1;
|
||||
setDirection(direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow()
|
||||
{
|
||||
if (midnightTimer != null) midnightTimer.removeListener(this);
|
||||
if (prefs != null) prefs.removeListener(this);
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
private void createButtons()
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
removeAllViews();
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = (int) getContext()
|
||||
.getResources()
|
||||
.getDimension(R.dimen.checkmarkHeight);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
Resources res = getContext().getResources();
|
||||
float width = res.getDimension(R.dimen.checkmarkWidth);
|
||||
float height = res.getDimension(R.dimen.checkmarkHeight);
|
||||
boolean reverse = shouldReverseCheckmarks();
|
||||
boolean isRtl = InterfaceUtils.isLayoutRtl(this);
|
||||
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -getDataOffset());
|
||||
float em = paint.measureText("m");
|
||||
|
||||
for (int i = 0; i < buttonCount; i++)
|
||||
addView(
|
||||
inflate(context, R.layout.list_habits_header_checkmark, null));
|
||||
|
||||
for (int i = 0; i < getChildCount(); i++)
|
||||
{
|
||||
int position = i;
|
||||
if (shouldReverseCheckmarks()) position = getChildCount() - i - 1;
|
||||
rect.set(0, 0, width, height);
|
||||
rect.offset(canvas.getWidth(), 0);
|
||||
|
||||
View button = getChildAt(position);
|
||||
TextView label = (TextView) button.findViewById(R.id.tvCheck);
|
||||
label.setText(DateUtils.formatHeaderDate(day));
|
||||
if(reverse) rect.offset(- (i + 1) * width, 0);
|
||||
else rect.offset((i - buttonCount) * width, 0);
|
||||
|
||||
if (isRtl) rect.set(canvas.getWidth() - rect.right, rect.top,
|
||||
canvas.getWidth() - rect.left, rect.bottom);
|
||||
|
||||
String text = DateUtils.formatHeaderDate(day).toUpperCase();
|
||||
String[] lines = text.split("\n");
|
||||
|
||||
int y1 = (int)(rect.centerY() - 0.25 * em);
|
||||
int y2 = (int)(rect.centerY() + 1.25 * em);
|
||||
|
||||
canvas.drawText(lines[0], rect.centerX(), y1, paint);
|
||||
canvas.drawText(lines[1], rect.centerX(), y2, paint);
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.habits.show.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
@@ -24,6 +24,10 @@ import android.view.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@@ -33,12 +37,38 @@ public class ShowHabitsMenu extends BaseMenu
|
||||
@NonNull
|
||||
private final ShowHabitScreen screen;
|
||||
|
||||
@NonNull
|
||||
private final Habit habit;
|
||||
|
||||
@NonNull
|
||||
private final TaskRunner taskRunner;
|
||||
|
||||
@NonNull
|
||||
private ExportCSVTaskFactory exportCSVFactory;
|
||||
|
||||
@Inject
|
||||
public ShowHabitsMenu(@NonNull BaseActivity activity,
|
||||
@NonNull ShowHabitScreen screen)
|
||||
@NonNull ShowHabitScreen screen,
|
||||
@NonNull Habit habit,
|
||||
@NonNull ExportCSVTaskFactory exportCSVFactory,
|
||||
@NonNull TaskRunner taskRunner)
|
||||
{
|
||||
super(activity);
|
||||
this.screen = screen;
|
||||
this.habit = habit;
|
||||
this.taskRunner = taskRunner;
|
||||
this.exportCSVFactory = exportCSVFactory;
|
||||
}
|
||||
|
||||
public void exportHabit()
|
||||
{
|
||||
List<Habit> selected = new LinkedList<>();
|
||||
selected.add(habit);
|
||||
ExportCSVTask task = exportCSVFactory.create(selected, filename -> {
|
||||
if (filename != null) screen.showSendFileScreen(filename);
|
||||
else screen.showMessage(R.string.could_not_export);
|
||||
});
|
||||
taskRunner.execute(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +80,10 @@ public class ShowHabitsMenu extends BaseMenu
|
||||
screen.showEditHabitDialog();
|
||||
return true;
|
||||
|
||||
case R.id.export:
|
||||
this.exportHabit();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.preferences.*;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.content.res.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
|
||||
@@ -22,12 +22,16 @@ package org.isoron.uhabits.activities.settings;
|
||||
import android.app.backup.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.provider.*;
|
||||
import android.support.v7.preference.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.habits.list.*;
|
||||
import org.isoron.uhabits.notifications.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import static android.os.Build.VERSION.*;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
@@ -62,8 +66,13 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
|
||||
updateRingtoneDescription();
|
||||
|
||||
if (InterfaceUtils.isLocaleFullyTranslated())
|
||||
removePreference("translate", "linksCategory");
|
||||
if (SDK_INT < Build.VERSION_CODES.O)
|
||||
findPreference("reminderCustomize").setVisible(false);
|
||||
else
|
||||
{
|
||||
findPreference("reminderSound").setVisible(false);
|
||||
findPreference("pref_snooze_interval").setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,6 +100,17 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
RINGTONE_REQUEST_CODE);
|
||||
return true;
|
||||
}
|
||||
else if (key.equals("reminderCustomize"))
|
||||
{
|
||||
if (SDK_INT < Build.VERSION_CODES.O) return true;
|
||||
|
||||
NotificationTray.createAndroidNotificationChannel(getContext());
|
||||
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
@@ -110,14 +130,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
BackupManager.dataChanged("org.isoron.uhabits");
|
||||
}
|
||||
|
||||
private void removePreference(String preferenceKey, String categoryKey)
|
||||
{
|
||||
PreferenceCategory cat =
|
||||
(PreferenceCategory) findPreference(categoryKey);
|
||||
Preference pref = findPreference(preferenceKey);
|
||||
cat.removePreference(pref);
|
||||
}
|
||||
|
||||
private void setResultOnPreferenceClick(String key, final int result)
|
||||
{
|
||||
Preference pref = findPreference(key);
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.support.v7.widget.*;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
@@ -39,6 +39,12 @@ public class IntentFactory
|
||||
{
|
||||
}
|
||||
|
||||
public Intent helpTranslate(Context context)
|
||||
{
|
||||
String url = context.getString(R.string.translateURL);
|
||||
return buildViewIntent(url);
|
||||
}
|
||||
|
||||
public Intent rateApp(Context context)
|
||||
{
|
||||
String url = context.getString(R.string.playStoreURL);
|
||||
|
||||
@@ -119,7 +119,7 @@ public class HabitsCSVExporter
|
||||
{
|
||||
String sane = sanitizeFilename(h.getName());
|
||||
String habitDirName =
|
||||
String.format("%03d %s", allHabits.indexOf(h) + 1, sane);
|
||||
String.format(Locale.US, "%03d %s", allHabits.indexOf(h) + 1, sane);
|
||||
habitDirName = habitDirName.trim() + "/";
|
||||
|
||||
new File(exportDirName + habitDirName).mkdirs();
|
||||
@@ -202,7 +202,7 @@ public class HabitsCSVExporter
|
||||
checksWriter.write(String.valueOf(checkmarks.get(j)[i]));
|
||||
checksWriter.write(DELIMITER);
|
||||
String score =
|
||||
String.format("%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE);
|
||||
String.format(Locale.US, "%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE);
|
||||
scoresWriter.write(score);
|
||||
scoresWriter.write(DELIMITER);
|
||||
}
|
||||
|
||||
@@ -19,18 +19,20 @@
|
||||
|
||||
package org.isoron.uhabits.io;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.*;
|
||||
import android.database.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import com.activeandroid.ActiveAndroid;
|
||||
import com.activeandroid.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.DatabaseUtils;
|
||||
import org.isoron.uhabits.utils.FileUtils;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@@ -39,10 +41,15 @@ import javax.inject.*;
|
||||
*/
|
||||
public class LoopDBImporter extends AbstractImporter
|
||||
{
|
||||
@NonNull
|
||||
private Context context;
|
||||
|
||||
@Inject
|
||||
public LoopDBImporter(@NonNull HabitList habits)
|
||||
public LoopDBImporter(@NonNull @AppContext Context context,
|
||||
@NonNull HabitList habits)
|
||||
{
|
||||
super(habits);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,23 +60,37 @@ public class LoopDBImporter extends AbstractImporter
|
||||
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
|
||||
SQLiteDatabase.OPEN_READONLY);
|
||||
|
||||
boolean canHandle = true;
|
||||
|
||||
Cursor c = db.rawQuery(
|
||||
"select count(*) from SQLITE_MASTER where name=? or name=?",
|
||||
new String[]{"Checkmarks", "Repetitions"});
|
||||
new String[]{ "Checkmarks", "Repetitions" });
|
||||
|
||||
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
|
||||
if (!c.moveToFirst() || c.getInt(0) != 2)
|
||||
{
|
||||
Log.w("LoopDBImporter", "Cannot handle file: tables not found");
|
||||
canHandle = false;
|
||||
}
|
||||
|
||||
if (db.getVersion() > BuildConfig.databaseVersion)
|
||||
{
|
||||
Log.w("LoopDBImporter", String.format(
|
||||
"Cannot handle file: incompatible version: %d > %d",
|
||||
db.getVersion(), BuildConfig.databaseVersion));
|
||||
canHandle = false;
|
||||
}
|
||||
|
||||
c.close();
|
||||
db.close();
|
||||
return result;
|
||||
return canHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importHabitsFromFile(@NonNull File file) throws IOException
|
||||
{
|
||||
ActiveAndroid.dispose();
|
||||
File originalDB = DatabaseUtils.getDatabaseFile();
|
||||
File originalDB = DatabaseUtils.getDatabaseFile(context);
|
||||
FileUtils.copy(file, originalDB);
|
||||
DatabaseUtils.initializeActiveAndroid();
|
||||
DatabaseUtils.initializeActiveAndroid(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public abstract class CheckmarkList
|
||||
*
|
||||
* @return value of today's checkmark
|
||||
*/
|
||||
public final int getTodayValue()
|
||||
public int getTodayValue()
|
||||
{
|
||||
Checkmark today = getToday();
|
||||
if (today != null) return today.getValue();
|
||||
@@ -192,7 +192,7 @@ public abstract class CheckmarkList
|
||||
Checkmark newest = getNewestComputed();
|
||||
Checkmark oldest = getOldestComputed();
|
||||
|
||||
if (newest == null)
|
||||
if (newest == null || oldest == null)
|
||||
{
|
||||
forceRecompute(from, to);
|
||||
}
|
||||
@@ -208,6 +208,7 @@ public abstract class CheckmarkList
|
||||
*
|
||||
* @return oldest checkmark already computed
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Checkmark getOldestComputed();
|
||||
|
||||
/**
|
||||
@@ -285,5 +286,6 @@ public abstract class CheckmarkList
|
||||
*
|
||||
* @return newest checkmark already computed
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Checkmark getNewestComputed();
|
||||
}
|
||||
|
||||
@@ -48,9 +48,7 @@ public abstract class HabitList implements Iterable<Habit>
|
||||
public HabitList()
|
||||
{
|
||||
observable = new ModelObservable();
|
||||
filter = new HabitMatcherBuilder()
|
||||
.setArchivedAllowed(true)
|
||||
.build();
|
||||
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
|
||||
}
|
||||
|
||||
protected HabitList(@NonNull HabitMatcher filter)
|
||||
@@ -106,6 +104,15 @@ public abstract class HabitList implements Iterable<Habit>
|
||||
return observable;
|
||||
}
|
||||
|
||||
public abstract Order getOrder();
|
||||
|
||||
/**
|
||||
* Changes the order of the elements on the list.
|
||||
*
|
||||
* @param order the new order criterea
|
||||
*/
|
||||
public abstract void setOrder(@NonNull Order order);
|
||||
|
||||
/**
|
||||
* Returns the index of the given habit in the list, or -1 if the list does
|
||||
* not contain the habit.
|
||||
@@ -149,7 +156,7 @@ public abstract class HabitList implements Iterable<Habit>
|
||||
|
||||
public void repair()
|
||||
{
|
||||
for(Habit h : this)
|
||||
for (Habit h : this)
|
||||
{
|
||||
h.getCheckmarks().invalidateNewerThan(0);
|
||||
h.getStreaks().invalidateNewerThan(0);
|
||||
@@ -228,4 +235,12 @@ public abstract class HabitList implements Iterable<Habit>
|
||||
|
||||
csv.close();
|
||||
}
|
||||
|
||||
public enum Order
|
||||
{
|
||||
BY_NAME,
|
||||
BY_COLOR,
|
||||
BY_SCORE,
|
||||
BY_POSITION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public abstract class ScoreList implements Iterable<Score>
|
||||
* @param timestamp the timestamp of a day
|
||||
* @return score value for that day
|
||||
*/
|
||||
public final int getValue(long timestamp)
|
||||
public synchronized final int getValue(long timestamp)
|
||||
{
|
||||
compute(timestamp, timestamp);
|
||||
Score s = getComputedByTimestamp(timestamp);
|
||||
@@ -173,7 +173,7 @@ public abstract class ScoreList implements Iterable<Score>
|
||||
{
|
||||
String timestamp = dateFormat.format(s.getTimestamp());
|
||||
String score =
|
||||
String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE);
|
||||
String.format(Locale.US, "%.4f", ((float) s.getValue()) / Score.MAX_VALUE);
|
||||
out.write(String.format("%s,%s\n", timestamp, score));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ public class MemoryCheckmarkList extends CheckmarkList
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Checkmark getOldestComputed()
|
||||
{
|
||||
if(list.isEmpty()) return null;
|
||||
@@ -79,6 +80,7 @@ public class MemoryCheckmarkList extends CheckmarkList
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Checkmark getNewestComputed()
|
||||
{
|
||||
if(list.isEmpty()) return null;
|
||||
|
||||
@@ -25,6 +25,8 @@ import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.models.HabitList.Order.*;
|
||||
|
||||
/**
|
||||
* In-memory implementation of {@link HabitList}.
|
||||
*/
|
||||
@@ -33,16 +35,23 @@ public class MemoryHabitList extends HabitList
|
||||
@NonNull
|
||||
private LinkedList<Habit> list;
|
||||
|
||||
private Comparator<Habit> comparator = null;
|
||||
|
||||
@NonNull
|
||||
private Order order;
|
||||
|
||||
public MemoryHabitList()
|
||||
{
|
||||
super();
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
protected MemoryHabitList(@NonNull HabitMatcher matcher)
|
||||
{
|
||||
super(matcher);
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,6 +66,7 @@ public class MemoryHabitList extends HabitList
|
||||
|
||||
if (id == null) habit.setId((long) list.size());
|
||||
list.addLast(habit);
|
||||
resort();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,10 +92,17 @@ public class MemoryHabitList extends HabitList
|
||||
public HabitList getFiltered(HabitMatcher matcher)
|
||||
{
|
||||
MemoryHabitList habits = new MemoryHabitList(matcher);
|
||||
for(Habit h : this) if (matcher.matches(h)) habits.add(h);
|
||||
habits.comparator = comparator;
|
||||
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
|
||||
return habits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Order getOrder()
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(@NonNull Habit h)
|
||||
{
|
||||
@@ -112,6 +129,14 @@ public class MemoryHabitList extends HabitList
|
||||
list.add(toPos, from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
this.comparator = getComparatorByOrder(order);
|
||||
resort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
@@ -123,4 +148,34 @@ public class MemoryHabitList extends HabitList
|
||||
{
|
||||
// NOP
|
||||
}
|
||||
|
||||
private Comparator<Habit> getComparatorByOrder(Order order)
|
||||
{
|
||||
Comparator<Habit> nameComparator =
|
||||
(h1, h2) -> h1.getName().compareTo(h2.getName());
|
||||
|
||||
Comparator<Habit> colorComparator = (h1, h2) -> {
|
||||
Integer c1 = h1.getColor();
|
||||
Integer c2 = h2.getColor();
|
||||
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
|
||||
else return c1.compareTo(c2);
|
||||
};
|
||||
|
||||
Comparator<Habit> scoreComparator = (h1, h2) -> {
|
||||
int s1 = h1.getScores().getTodayValue();
|
||||
int s2 = h2.getScores().getTodayValue();
|
||||
return Integer.compare(s2, s1);
|
||||
};
|
||||
|
||||
if (order == BY_POSITION) return null;
|
||||
if (order == BY_NAME) return nameComparator;
|
||||
if (order == BY_COLOR) return colorComparator;
|
||||
if (order == BY_SCORE) return scoreComparator;
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
private void resort()
|
||||
{
|
||||
if (comparator != null) Collections.sort(list, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
public class InvalidDatabaseVersionException extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import android.support.annotation.*;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.query.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
@@ -38,12 +37,22 @@ import java.util.*;
|
||||
*/
|
||||
public class SQLiteCheckmarkList extends CheckmarkList
|
||||
{
|
||||
|
||||
private static final String ADD_QUERY =
|
||||
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
|
||||
|
||||
private static final String INVALIDATE_QUERY =
|
||||
"delete from Checkmarks where habit = ? and timestamp >= ?";
|
||||
|
||||
@Nullable
|
||||
private HabitRecord habitRecord;
|
||||
|
||||
@NonNull
|
||||
private final SQLiteUtils<CheckmarkRecord> sqlite;
|
||||
|
||||
@Nullable
|
||||
private CachedData cache;
|
||||
|
||||
public SQLiteCheckmarkList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
@@ -54,16 +63,11 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
public void add(List<Checkmark> checkmarks)
|
||||
{
|
||||
check(habit.getId());
|
||||
|
||||
String query =
|
||||
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
SQLiteStatement statement = db.compileStatement(ADD_QUERY);
|
||||
db.beginTransaction();
|
||||
try
|
||||
{
|
||||
SQLiteStatement statement = db.compileStatement(query);
|
||||
|
||||
for (Checkmark c : checkmarks)
|
||||
{
|
||||
statement.bindLong(1, habit.getId());
|
||||
@@ -87,8 +91,7 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
check(habit.getId());
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from checkmarks " +
|
||||
String query = "select habit, timestamp, value from checkmarks " +
|
||||
"where habit = ? and timestamp >= ? and timestamp <= ? " +
|
||||
"order by timestamp desc";
|
||||
|
||||
@@ -101,26 +104,28 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
List<CheckmarkRecord> records = sqlite.query(query, params);
|
||||
for (CheckmarkRecord record : records) record.habit = habitRecord;
|
||||
|
||||
int nDays = DateUtils.getDaysBetween(fromTimestamp, toTimestamp) + 1;
|
||||
if (records.size() != nDays)
|
||||
{
|
||||
throw new InconsistentDatabaseException(
|
||||
String.format("habit=%s, %d expected, %d found",
|
||||
habit.getName(), nDays, records.size()));
|
||||
records = fixRecords(records, habitRecord, fromTimestamp, toTimestamp);
|
||||
return toCheckmarks(records);
|
||||
}
|
||||
|
||||
return toCheckmarks(records);
|
||||
@Override
|
||||
public int getTodayValue()
|
||||
{
|
||||
if (cache == null || cache.expired())
|
||||
cache = new CachedData(super.getTodayValue());
|
||||
|
||||
return cache.todayValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
new Delete()
|
||||
.from(CheckmarkRecord.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp >= ?", timestamp)
|
||||
.execute();
|
||||
|
||||
cache = null;
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
|
||||
statement.bindLong(1, habit.getId());
|
||||
statement.bindLong(2, timestamp);
|
||||
statement.execute();
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@@ -129,10 +134,8 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
protected Checkmark getNewestComputed()
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from checkmarks " +
|
||||
"where habit = ? " +
|
||||
"order by timestamp desc " +
|
||||
String query = "select habit, timestamp, value from checkmarks " +
|
||||
"where habit = ? " + "order by timestamp desc " +
|
||||
"limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
@@ -140,13 +143,12 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Checkmark getOldestComputed()
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, value " +
|
||||
"from checkmarks " +
|
||||
"where habit = ? " +
|
||||
"order by timestamp asc " +
|
||||
String query = "select habit, timestamp, value from checkmarks " +
|
||||
"where habit = ? " + "order by timestamp asc " +
|
||||
"limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
@@ -179,4 +181,44 @@ public class SQLiteCheckmarkList extends CheckmarkList
|
||||
for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark());
|
||||
return checkmarks;
|
||||
}
|
||||
|
||||
public static List<CheckmarkRecord> fixRecords(List<CheckmarkRecord> original,
|
||||
HabitRecord habit,
|
||||
long fromTimestamp,
|
||||
long toTimestamp)
|
||||
{
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
ArrayList<CheckmarkRecord> records = new ArrayList<>();
|
||||
|
||||
for (long t = toTimestamp; t >= fromTimestamp; t -= day)
|
||||
records.add(new CheckmarkRecord(habit, t, Checkmark.UNCHECKED));
|
||||
|
||||
for (CheckmarkRecord record : original)
|
||||
{
|
||||
if ((toTimestamp - record.timestamp) % day != 0) continue;
|
||||
int offset = (int) ((toTimestamp - record.timestamp) / day);
|
||||
if (offset < 0 || offset >= records.size()) continue;
|
||||
records.set(offset, record);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private static class CachedData
|
||||
{
|
||||
int todayValue;
|
||||
|
||||
private long today;
|
||||
|
||||
CachedData(int todayValue)
|
||||
{
|
||||
this.todayValue = todayValue;
|
||||
this.today = DateUtils.getStartOfToday();
|
||||
}
|
||||
|
||||
boolean expired()
|
||||
{
|
||||
return today != DateUtils.getStartOfToday();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,15 @@ public class SQLiteHabitList extends HabitList
|
||||
|
||||
private static SQLiteHabitList instance;
|
||||
|
||||
@NonNull
|
||||
private final SQLiteUtils<HabitRecord> sqlite;
|
||||
|
||||
@NonNull
|
||||
private final ModelFactory modelFactory;
|
||||
|
||||
@NonNull
|
||||
private Order order;
|
||||
|
||||
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
|
||||
{
|
||||
super();
|
||||
@@ -50,16 +55,19 @@ public class SQLiteHabitList extends HabitList
|
||||
|
||||
if (cache == null) cache = new HashMap<>();
|
||||
sqlite = new SQLiteUtils<>(HabitRecord.class);
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
|
||||
@NonNull HabitMatcher filter)
|
||||
@NonNull HabitMatcher filter,
|
||||
@NonNull Order order)
|
||||
{
|
||||
super(filter);
|
||||
this.modelFactory = modelFactory;
|
||||
|
||||
if (cache == null) cache = new HashMap<>();
|
||||
sqlite = new SQLiteUtils<>(HabitRecord.class);
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public static SQLiteHabitList getInstance(
|
||||
@@ -118,7 +126,20 @@ public class SQLiteHabitList extends HabitList
|
||||
@Override
|
||||
public HabitList getFiltered(HabitMatcher filter)
|
||||
{
|
||||
return new SQLiteHabitList(modelFactory, filter);
|
||||
return new SQLiteHabitList(modelFactory, filter, order);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Order getOrder()
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,6 +235,13 @@ public class SQLiteHabitList extends HabitList
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repair()
|
||||
{
|
||||
super.repair();
|
||||
rebuildOrder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
@@ -233,7 +261,7 @@ public class SQLiteHabitList extends HabitList
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Habit> toList()
|
||||
protected synchronized List<Habit> toList()
|
||||
{
|
||||
String query = buildSelectQuery();
|
||||
List<HabitRecord> recordList = sqlite.query(query, null);
|
||||
@@ -242,19 +270,43 @@ public class SQLiteHabitList extends HabitList
|
||||
for (HabitRecord record : recordList)
|
||||
{
|
||||
Habit habit = getById(record.getId());
|
||||
if (habit == null)
|
||||
throw new RuntimeException("habit not in database");
|
||||
|
||||
if (habit == null) continue;
|
||||
if (!filter.matches(habit)) continue;
|
||||
habits.add(habit);
|
||||
}
|
||||
|
||||
if(order == Order.BY_SCORE)
|
||||
{
|
||||
Collections.sort(habits, (lhs, rhs) -> {
|
||||
int s1 = lhs.getScores().getTodayValue();
|
||||
int s2 = rhs.getScores().getTodayValue();
|
||||
return Integer.compare(s2, s1);
|
||||
});
|
||||
}
|
||||
|
||||
return habits;
|
||||
}
|
||||
|
||||
private void appendOrderBy(StringBuilder query)
|
||||
{
|
||||
switch (order)
|
||||
{
|
||||
case BY_POSITION:
|
||||
query.append("order by position ");
|
||||
break;
|
||||
|
||||
case BY_NAME:
|
||||
case BY_SCORE:
|
||||
query.append("order by name ");
|
||||
break;
|
||||
|
||||
case BY_COLOR:
|
||||
query.append("order by color, name ");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSelect(StringBuilder query)
|
||||
@@ -282,11 +334,4 @@ public class SQLiteHabitList extends HabitList
|
||||
appendOrderBy(query);
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void repair()
|
||||
{
|
||||
super.repair();
|
||||
rebuildOrder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
|
||||
package org.isoron.uhabits.models.sqlite;
|
||||
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.*;
|
||||
import android.database.sqlite.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.query.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
@@ -43,10 +43,16 @@ public class SQLiteRepetitionList extends RepetitionList
|
||||
@Nullable
|
||||
private HabitRecord habitRecord;
|
||||
|
||||
private SQLiteStatement addStatement;
|
||||
|
||||
public SQLiteRepetitionList(@NonNull Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
sqlite = new SQLiteUtils<>(RepetitionRecord.class);
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
String addQuery = "insert into repetitions(habit, timestamp) values (?,?)";
|
||||
addStatement = db.compileStatement(addQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,11 +67,9 @@ public class SQLiteRepetitionList extends RepetitionList
|
||||
public void add(Repetition rep)
|
||||
{
|
||||
check(habit.getId());
|
||||
|
||||
RepetitionRecord record = new RepetitionRecord();
|
||||
record.copyFrom(rep);
|
||||
record.habit = habitRecord;
|
||||
record.save();
|
||||
addStatement.bindLong(1, habit.getId());
|
||||
addStatement.bindLong(2, rep.getTimestamp());
|
||||
addStatement.execute();
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.support.annotation.*;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.*;
|
||||
import com.activeandroid.query.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.models.sqlite.records.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.util.*;
|
||||
@@ -37,12 +37,21 @@ import java.util.*;
|
||||
*/
|
||||
public class SQLiteScoreList extends ScoreList
|
||||
{
|
||||
public static final String ADD_QUERY =
|
||||
"insert into Score(habit, timestamp, score) values (?,?,?)";
|
||||
|
||||
public static final String INVALIDATE_QUERY =
|
||||
"delete from Score where habit = ? and timestamp >= ?";
|
||||
|
||||
@Nullable
|
||||
private HabitRecord habitRecord;
|
||||
|
||||
@NonNull
|
||||
private final SQLiteUtils<ScoreRecord> sqlite;
|
||||
|
||||
@Nullable
|
||||
private CachedData cache = null;
|
||||
|
||||
/**
|
||||
* Constructs a new ScoreList associated with the given habit.
|
||||
*
|
||||
@@ -58,16 +67,11 @@ public class SQLiteScoreList extends ScoreList
|
||||
public void add(List<Score> scores)
|
||||
{
|
||||
check(habit.getId());
|
||||
String query =
|
||||
"insert into Score(habit, timestamp, score) values (?,?,?)";
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
SQLiteStatement statement = db.compileStatement(ADD_QUERY);
|
||||
db.beginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
SQLiteStatement statement = db.compileStatement(query);
|
||||
|
||||
for (Score s : scores)
|
||||
{
|
||||
statement.bindLong(1, habit.getId());
|
||||
@@ -86,13 +90,13 @@ public class SQLiteScoreList extends ScoreList
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
public synchronized List<Score> getByInterval(long fromTimestamp,
|
||||
long toTimestamp)
|
||||
{
|
||||
check(habit.getId());
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
String query = "select habit, timestamp, score " +
|
||||
"from Score " +
|
||||
String query = "select habit, timestamp, score from Score " +
|
||||
"where habit = ? and timestamp >= ? and timestamp <= ? " +
|
||||
"order by timestamp desc";
|
||||
|
||||
@@ -124,14 +128,23 @@ public class SQLiteScoreList extends ScoreList
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateNewerThan(long timestamp)
|
||||
public synchronized int getTodayValue()
|
||||
{
|
||||
new Delete()
|
||||
.from(ScoreRecord.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp >= ?", timestamp)
|
||||
.execute();
|
||||
if (cache == null || cache.expired())
|
||||
cache = new CachedData(super.getTodayValue());
|
||||
|
||||
return cache.todayValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
cache = null;
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
|
||||
statement.bindLong(1, habit.getId());
|
||||
statement.bindLong(2, timestamp);
|
||||
statement.execute();
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@@ -159,8 +172,7 @@ public class SQLiteScoreList extends ScoreList
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, score from Score " +
|
||||
"where habit = ? order by timestamp desc " +
|
||||
"limit 1";
|
||||
"where habit = ? order by timestamp desc limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
return getScoreFromQuery(query, params);
|
||||
@@ -172,8 +184,7 @@ public class SQLiteScoreList extends ScoreList
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp, score from Score " +
|
||||
"where habit = ? order by timestamp asc " +
|
||||
"limit 1";
|
||||
"where habit = ? order by timestamp asc limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
return getScoreFromQuery(query, params);
|
||||
@@ -204,4 +215,22 @@ public class SQLiteScoreList extends ScoreList
|
||||
for (ScoreRecord r : records) scores.add(r.toScore());
|
||||
return scores;
|
||||
}
|
||||
|
||||
private static class CachedData
|
||||
{
|
||||
int todayValue;
|
||||
|
||||
private long today;
|
||||
|
||||
CachedData(int todayValue)
|
||||
{
|
||||
this.todayValue = todayValue;
|
||||
this.today = DateUtils.getStartOfToday();
|
||||
}
|
||||
|
||||
boolean expired()
|
||||
{
|
||||
return today != DateUtils.getStartOfToday();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user