Compare commits

..

62 Commits

Author SHA1 Message Date
957a5b7c17 Fix CSV export in some locales; bump version to 1.7.11 (38) 2019-08-10 11:42:25 -05:00
64cc9f78a8 Increase targetSdk to 28 2019-06-15 09:46:06 -05:00
e50c411d1e Fix crash preventing some Xiaomi devices from showing notifications 2019-06-15 08:57:11 -05:00
ce27773138 Merge branch 'hotfix/1.7.9' 2018-04-21 10:31:54 -05:00
0864f83307 Update CHANGELOG 2018-04-21 10:17:38 -05:00
624cc67d9b Update translations 2018-04-21 10:17:31 -05:00
462bac8167 Add support for adaptive icons
Closes #395
2018-04-21 09:39:30 -05:00
5865eb41f7 Update notification for Android Oreo
- Add link to notification channel settings
- Remove snooze button

Closes #400
2018-04-21 08:50:53 -05:00
2bfd4a942d Remove unused PebbleReceiverTest 2018-04-21 08:00:00 -05:00
b4a33cba39 Add ActiveAndroid source code to our tree and remove content providers
ActiveAndroid is not actively maintained anymore and contains code
related to that Content Providers that makes the application crash on
Android Oreo.
2018-04-14 10:22:31 -05:00
f02c86e61b Bump targetSdkVersion to 27 2018-04-14 10:19:34 -05:00
5021f50e18 Bump version to 1.7.9 (36) 2018-04-14 08:11:22 -05:00
bf3964a231 Merge branch 'hotfix/1.7.7' 2017-09-30 13:27:45 -05:00
657cde75d8 Disable signing config 2017-09-30 13:18:26 -05:00
c56b86d32c Bump version again
The previous number was already in use by an alpha version
2017-09-30 13:14:42 -05:00
16f20d50a0 Fix bug that caused reminders to keep repeating every few seconds
Fixes #351
2017-09-30 13:11:40 -05:00
7bb88dcb97 Bump version and update CHANGELOG 2017-09-30 12:59:02 -05:00
7bb62c197f Include Play Store plugin 2017-07-26 10:07:25 -04:00
540a618ba8 Merge branch 'hotfix/1.7.6' 2017-07-18 22:45:50 -04:00
bf24cc608c Update changelog 2017-07-18 22:45:36 -04:00
a73459784e Fix NPE in HabitCardView.triggerRipple 2017-07-18 22:16:11 -04:00
de3b97dfdf Fix IndexOutOfBoundsException in HabitCardListAdapter 2017-07-18 22:13:10 -04:00
91996924d9 Never store reference to SQLiteDatabase
SQLiteDatabase closes automatically at random times. Storing a reference
to an open SQLiteDatabase eventually leads to a crash.
See: https://stackoverflow.com/questions/1483629/
2017-07-18 22:07:21 -04:00
9fe446b424 Fix crash when tapping HistoryChart 2017-07-18 21:55:15 -04:00
3857eaf5e9 Update changelog 2017-07-18 21:47:25 -04:00
e29fb58922 Fix failing tests 2017-07-18 21:47:19 -04:00
404fc869b0 Repair inconsistent data instead of throwing exception 2017-07-18 21:28:48 -04:00
001dd5a7c1 Update translations 2017-07-18 18:39:20 -04:00
7930cc8f31 Bump version to 1.7.6 2017-07-18 17:52:21 -04:00
526830ba61 Merge branch 'hotfix/1.7.5' 2017-06-10 18:56:22 -04:00
fe1513bb64 Fix race conditions on SQLiteScoreList 2017-06-10 16:48:53 -04:00
e06ace9ea8 Fix ArrayIndexOutOfBoundsException on FrequencyChart 2017-06-10 15:38:21 -04:00
d727dabb2b Fix snooze button on notifications 2017-06-10 15:30:51 -04:00
d17e8fcbfb Bump version to 1.7.5 2017-06-10 15:28:12 -04:00
be3d7145ab Merge branch 'hotfix/1.7.4' 2017-06-04 17:36:17 -04:00
cf66587644 ScoreList: make getValue synchronized 2017-06-04 17:29:41 -04:00
0dc9ec2e5f Expire caches on SQLite lists 2017-06-04 17:24:18 -04:00
0a375ded96 Silently ignore missing habits 2017-06-04 17:07:07 -04:00
fa5d6f8fee Bump version 2017-06-04 17:01:40 -04:00
534e6c2d9d Merge branch 'hotfix/1.7.3' 2017-05-30 09:34:12 -04:00
b6501c9a29 Fix NullPointerException 2017-05-30 09:22:15 -04:00
238a1c724d Bump version and update changelog 2017-05-30 09:06:50 -04:00
34ca9d17a2 Make usage of BundleSavedState more robust 2017-05-30 09:05:05 -04:00
e844390614 Improve performance of repeated getTodayValue calls 2017-05-29 22:22:31 -04:00
5e00d07b73 Merge branch 'hotfix/1.7.2' 2017-05-28 00:41:37 -04:00
28b6ae7014 Apply dark theme on dialogs
Fixes #291
2017-05-28 00:31:48 -04:00
2a1bf5fc2e Update CHANGELOG 2017-05-27 23:38:58 -04:00
ef7483f9dc Make SQLiteHabitList.toList synchronized 2017-05-27 23:37:24 -04:00
bbb9ed8f99 Update translations 2017-05-27 23:30:45 -04:00
c49d576871 Fix crash at startup 2017-05-27 23:29:35 -04:00
bc66ae4f7a Bump version to 1.7.2 2017-05-27 23:28:16 -04:00
fa416adbb9 Merge branch 'hotfix/1.7.1' 2017-05-21 10:20:48 -04:00
8b835b9918 Update CHANGELOG 2017-05-21 10:10:30 -04:00
471c5d341f Update translations 2017-05-21 10:03:13 -04:00
57296745b3 Improve performance of CheckmarkButtonView 2017-04-19 14:33:59 -04:00
140ab34a76 Verify database version before importing 2017-04-11 22:03:16 -04:00
0d6ad26505 Ignore exception when habit is not found 2017-04-11 21:19:03 -04:00
65cc99dbf7 Switch from View.BaseSavedState to Support Library's AbsSavedState
See https://code.google.com/p/android/issues/detail?id=196430
2017-04-11 17:49:55 -04:00
6855ef9d5e Update translations 2017-04-11 17:26:06 -04:00
4c58b084c6 Fix missing dialog title 2017-04-11 17:19:18 -04:00
5c8e522646 Fix header labels for RTL languages such as Arabic 2017-04-11 17:14:25 -04:00
55da0759d4 Bump version 2017-04-11 16:58:35 -04:00
128 changed files with 6476 additions and 1126 deletions

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@ docs/
gen/
local.properties
crowdin.yaml
local
secret/

View File

@@ -1,5 +1,46 @@
# 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

View File

@@ -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 25
buildToolsVersion "25.0.2"
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 25
targetSdkVersion 28
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:25.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:25.3.0'
compile 'com.android.support:design:25.3.0'
compile 'com.android.support:preference-v14:25.3.0'
compile 'com.android.support:support-v4:25.3.0'
compile 'com.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'
//}

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,29 +61,6 @@ public class SQLiteScoreListTest extends BaseAndroidTest
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testGetAll()
{
List<Score> list = scores.toList();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
@Test
public void testAdd()
{
@@ -101,6 +78,15 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(records.get(0).timestamp, equalTo(today));
}
@Test
public void testGetAll()
{
List<Score> list = scores.toList();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval()
{
@@ -115,6 +101,16 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval_concurrent() throws Exception
{
Runnable block1 = () -> scores.invalidateNewerThan(0);
Runnable block2 =
() -> assertThat(scores.getByInterval(today, today).size(),
equalTo(1));
runConcurrently(block1, block2);
}
@Test
public void testGetByInterval_withLongInterval()
{
@@ -125,6 +121,30 @@ public class SQLiteScoreListTest extends BaseAndroidTest
assertThat(list.size(), equalTo(201));
}
@Test
public void testGetTodayValue_concurrent() throws Exception
{
Runnable block1 = () -> scores.invalidateNewerThan(0);
Runnable block2 =
() -> assertThat(scores.getTodayValue(), equalTo(18407827));
runConcurrently(block1, block2);
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
private List<ScoreRecord> getAllRecords()
{
return new Select()

View File

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

View File

@@ -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);
}
}

View File

@@ -21,8 +21,8 @@
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="27"
android:versionName="1.7.0">
android:versionCode="38"
android:versionName="1.7.11">
<uses-permission android:name="android.permission.VIBRATE"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View 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);
}
}

View 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();
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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.
}
}

View 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);
}
}
}
}

View 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;
}
}

View 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 {};
}

View 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;
}

View 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 ";
}
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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();
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}
}

View 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;
}
}

View File

@@ -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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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 == ' ';
}
}

View 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;
}
}

View 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&lt;? extends T&gt; 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;
}
}
}

View File

@@ -21,10 +21,10 @@ package org.isoron.uhabits;
import android.app.*;
import android.content.*;
import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.tasks.*;
@@ -88,7 +88,16 @@ public class HabitsApplication extends Application
if (db.exists()) db.delete();
}
DatabaseUtils.initializeActiveAndroid(context);
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();

View File

@@ -20,25 +20,27 @@
package org.isoron.uhabits.activities.common.views;
import android.os.*;
import android.view.*;
import android.support.v4.os.*;
public class BundleSavedState extends View.BaseSavedState
public class BundleSavedState extends android.support.v4.view.AbsSavedState
{
public static final Parcelable.Creator<BundleSavedState> CREATOR =
new Parcelable.Creator<BundleSavedState>()
{
@Override
public BundleSavedState createFromParcel(Parcel source)
ParcelableCompat.newCreator(
new ParcelableCompatCreatorCallbacks<BundleSavedState>()
{
return new BundleSavedState(source);
}
@Override
public BundleSavedState createFromParcel(Parcel source,
ClassLoader loader)
{
return new BundleSavedState(source, loader);
}
@Override
public BundleSavedState[] newArray(int size)
{
return new BundleSavedState[size];
}
};
@Override
public BundleSavedState[] newArray(int size)
{
return new BundleSavedState[size];
}
});
public final Bundle bundle;
@@ -48,10 +50,10 @@ public class BundleSavedState extends View.BaseSavedState
this.bundle = bundle;
}
public BundleSavedState(Parcel source)
public BundleSavedState(Parcel source, ClassLoader loader)
{
super(source);
this.bundle = source.readBundle();
super(source, loader);
this.bundle = source.readBundle(loader);
}
@Override

View File

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

View File

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

View File

@@ -107,6 +107,12 @@ public abstract class ScrollableChart extends View
@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");

View File

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

View File

@@ -105,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);
@@ -314,6 +313,8 @@ public class HabitCardListAdapter
public void toggleSelection(int position)
{
Habit h = getItem(position);
if (h == null) return;
int k = selected.indexOf(h);
if (k < 0) selected.add(h);
else selected.remove(h);

View File

@@ -93,12 +93,12 @@ public class HabitCardListCache implements CommandRunner.Listener
* Returns the habits that occupies a certain position on the list.
*
* @param position the position of the habit
* @return the habit at given position
* @throws IndexOutOfBoundsException if position is not valid
* @return the habit at given position or null if position is invalid
*/
@NonNull
public Habit getHabitByPosition(int position)
@Nullable
public synchronized Habit getHabitByPosition(int position)
{
if(position < 0 || position >= data.habits.size()) return null;
return data.habits.get(position);
}

View File

@@ -20,21 +20,34 @@
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
public class CheckmarkButtonView extends TextView
import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.models.Checkmark.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
public class CheckmarkButtonView extends View
{
private int color;
private int value;
private StyledResources res;
private StyledResources styledRes;
private TextPaint paint;
private int lowContrastColor;
private RectF rect;
public CheckmarkButtonView(Context context)
{
@@ -42,6 +55,21 @@ public class CheckmarkButtonView extends TextView
init();
}
public CheckmarkButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
{
super(ctx, attrs);
init();
if(ctx == null) throw new IllegalStateException();
if(attrs == null) throw new IllegalStateException();
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(ColorUtils.getAndroidTestColor(paletteColor));
int value = getIntAttribute(ctx, attrs, "value", 0);
setValue(value);
}
public void setColor(int color)
{
this.color = color;
@@ -57,54 +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);
setMinHeight(
getResources().getDimensionPixelSize(R.dimen.checkmarkHeight));
setMinWidth(
getResources().getDimensionPixelSize(R.dimen.checkmarkWidth));
setFocusable(false);
setGravity(Gravity.CENTER);
setTypeface(InterfaceUtils.getFontAwesome(getContext()));
}
private void updateText()
{
int lowContrastColor = res.getColor(R.attr.lowContrastTextColor);
Resources res = getResources();
styledRes = new StyledResources(getContext());
if (value == Checkmark.CHECKED_EXPLICITLY)
{
setText(R.string.fa_check);
setTextColor(color);
}
paint = new TextPaint();
paint.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(res.getDimension(R.dimen.regularTextSize));
if (value == Checkmark.CHECKED_IMPLICITLY)
{
setText(R.string.fa_check);
setTextColor(lowContrastColor);
}
if (value == Checkmark.UNCHECKED)
{
setText(R.string.fa_times);
setTextColor(lowContrastColor);
}
rect = new RectF();
color = ColorUtils.getAndroidTestColor(0);
lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
}
}

View File

@@ -153,6 +153,12 @@ public class HabitCardListView extends RecyclerView
@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());

View File

@@ -143,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) - 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;
@@ -214,6 +215,7 @@ public class HabitCardView extends FrameLayout
scoreRing.setPercentage(rand.nextFloat());
checkmarkPanel.setColor(color);
checkmarkPanel.setCheckmarkValues(values);
checkmarkPanel.setButtonCount(5);
}
private void refresh()

View File

@@ -50,8 +50,6 @@ public class HeaderView extends ScrollableChart
private RectF rect;
private int maxDataOffset;
public HeaderView(Context context, AttributeSet attrs)
{
super(context, attrs);
@@ -76,7 +74,6 @@ public class HeaderView extends ScrollableChart
Resources res = context.getResources();
setScrollerBucketSize((int) res.getDimension(R.dimen.checkmarkWidth));
setDirection(shouldReverseCheckmarks() ? 1 : -1);
StyledResources sr = new StyledResources(context);
paint = new TextPaint();
@@ -99,7 +96,7 @@ public class HeaderView extends ScrollableChart
@Override
public void onCheckmarkOrderChanged()
{
setDirection(shouldReverseCheckmarks() ? 1 : -1);
updateDirection();
postInvalidate();
}
@@ -112,11 +109,20 @@ public class HeaderView extends ScrollableChart
@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()
{
@@ -145,6 +151,7 @@ public class HeaderView extends ScrollableChart
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");
@@ -153,9 +160,13 @@ public class HeaderView extends ScrollableChart
{
rect.set(0, 0, width, height);
rect.offset(canvas.getWidth(), 0);
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");

View File

@@ -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
{
@@ -61,6 +65,14 @@ public class SettingsFragment extends PreferenceFragmentCompat
setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT);
updateRingtoneDescription();
if (SDK_INT < Build.VERSION_CODES.O)
findPreference("reminderCustomize").setVisible(false);
else
{
findPreference("reminderSound").setVisible(false);
findPreference("pref_snooze_interval").setVisible(false);
}
}
@Override
@@ -88,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);
}

View File

@@ -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);
}

View File

@@ -19,20 +19,20 @@
package org.isoron.uhabits.io;
import android.content.Context;
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.AppContext;
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.*;
@@ -45,7 +45,8 @@ public class LoopDBImporter extends AbstractImporter
private Context context;
@Inject
public LoopDBImporter(@NonNull @AppContext Context context, @NonNull HabitList habits)
public LoopDBImporter(@NonNull @AppContext Context context,
@NonNull HabitList habits)
{
super(habits);
this.context = context;
@@ -59,15 +60,29 @@ 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

View File

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

View File

@@ -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));
}
}

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
@@ -38,12 +37,22 @@ import java.util.*;
*/
public class SQLiteCheckmarkList extends CheckmarkList
{
private static final String ADD_QUERY =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
private static final String INVALIDATE_QUERY =
"delete from Checkmarks where habit = ? and timestamp >= ?";
@Nullable
private HabitRecord habitRecord;
@NonNull
private final SQLiteUtils<CheckmarkRecord> sqlite;
@Nullable
private CachedData cache;
public SQLiteCheckmarkList(Habit habit)
{
super(habit);
@@ -54,16 +63,11 @@ public class SQLiteCheckmarkList extends CheckmarkList
public void add(List<Checkmark> checkmarks)
{
check(habit.getId());
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(ADD_QUERY);
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Checkmark c : checkmarks)
{
statement.bindLong(1, habit.getId());
@@ -87,8 +91,7 @@ public class SQLiteCheckmarkList extends CheckmarkList
check(habit.getId());
compute(fromTimestamp, toTimestamp);
String query = "select habit, timestamp, value " +
"from checkmarks " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"order by timestamp desc";
@@ -101,26 +104,28 @@ public class SQLiteCheckmarkList extends CheckmarkList
List<CheckmarkRecord> records = sqlite.query(query, params);
for (CheckmarkRecord record : records) record.habit = habitRecord;
int nDays = DateUtils.getDaysBetween(fromTimestamp, toTimestamp) + 1;
if (records.size() != nDays)
{
throw new InconsistentDatabaseException(
String.format("habit=%s, %d expected, %d found",
habit.getName(), nDays, records.size()));
}
records = fixRecords(records, habitRecord, fromTimestamp, toTimestamp);
return toCheckmarks(records);
}
@Override
public int getTodayValue()
{
if (cache == null || cache.expired())
cache = new CachedData(super.getTodayValue());
return cache.todayValue;
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
cache = null;
SQLiteDatabase db = Cache.openDatabase();
SQLiteStatement statement = db.compileStatement(INVALIDATE_QUERY);
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamp);
statement.execute();
observable.notifyListeners();
}
@@ -129,10 +134,8 @@ public class SQLiteCheckmarkList extends CheckmarkList
protected Checkmark getNewestComputed()
{
check(habit.getId());
String query = "select habit, timestamp, value " +
"from checkmarks " +
"where habit = ? " +
"order by timestamp desc " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? " + "order by timestamp desc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
@@ -140,13 +143,12 @@ public class SQLiteCheckmarkList extends CheckmarkList
}
@Override
@Nullable
protected Checkmark getOldestComputed()
{
check(habit.getId());
String query = "select habit, timestamp, value " +
"from checkmarks " +
"where habit = ? " +
"order by timestamp asc " +
String query = "select habit, timestamp, value from checkmarks " +
"where habit = ? " + "order by timestamp asc " +
"limit 1";
String params[] = { Long.toString(habit.getId()) };
@@ -179,4 +181,44 @@ public class SQLiteCheckmarkList extends CheckmarkList
for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark());
return checkmarks;
}
public static List<CheckmarkRecord> fixRecords(List<CheckmarkRecord> original,
HabitRecord habit,
long fromTimestamp,
long toTimestamp)
{
long day = DateUtils.millisecondsInOneDay;
ArrayList<CheckmarkRecord> records = new ArrayList<>();
for (long t = toTimestamp; t >= fromTimestamp; t -= day)
records.add(new CheckmarkRecord(habit, t, Checkmark.UNCHECKED));
for (CheckmarkRecord record : original)
{
if ((toTimestamp - record.timestamp) % day != 0) continue;
int offset = (int) ((toTimestamp - record.timestamp) / day);
if (offset < 0 || offset >= records.size()) continue;
records.set(offset, record);
}
return records;
}
private static class CachedData
{
int todayValue;
private long today;
CachedData(int todayValue)
{
this.todayValue = todayValue;
this.today = DateUtils.getStartOfToday();
}
boolean expired()
{
return today != DateUtils.getStartOfToday();
}
}
}

View File

@@ -261,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);
@@ -270,9 +270,7 @@ 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ package org.isoron.uhabits.notifications;
import android.app.*;
import android.content.*;
import android.graphics.*;
import android.os.*;
import android.support.annotation.*;
import android.support.v4.app.*;
import android.support.v4.app.NotificationCompat.*;
@@ -39,12 +40,15 @@ import java.util.*;
import javax.inject.*;
import static android.graphics.BitmapFactory.*;
import static android.os.Build.VERSION.*;
import static org.isoron.uhabits.utils.RingtoneUtils.*;
@AppScope
public class NotificationTray
implements CommandRunner.Listener, Preferences.Listener
{
public static final String REMINDERS_CHANNEL_ID = "REMINDERS";
@NonNull
private final Context context;
@@ -196,10 +200,6 @@ public class NotificationTray
context.getString(R.string.check),
pendingIntents.addCheckmark(habit, timestamp));
Action snoozeAction = new Action(R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
pendingIntents.snoozeNotification(habit));
Bitmap wearableBg =
decodeResource(context.getResources(), R.drawable.stripe);
@@ -208,30 +208,44 @@ public class NotificationTray
// WearableExtender.
WearableExtender wearableExtender = new WearableExtender()
.setBackground(wearableBg)
.addAction(checkAction)
.addAction(snoozeAction);
.addAction(checkAction);
Notification notification = new NotificationCompat.Builder(context)
Builder builder = new Builder(context, REMINDERS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.getName())
.setContentText(habit.getDescription())
.setContentIntent(pendingIntents.showHabit(habit))
.setDeleteIntent(pendingIntents.dismissNotification(habit))
.addAction(checkAction)
.addAction(snoozeAction)
.setSound(getRingtoneUri(context))
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.setOngoing(preferences.shouldMakeNotificationsSticky())
.build();
.setOngoing(preferences.shouldMakeNotificationsSticky());
if(SDK_INT < Build.VERSION_CODES.O) {
Action snoozeAction = new Action(R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
pendingIntents.snoozeNotification(habit));
wearableExtender.addAction(snoozeAction);
builder.addAction(snoozeAction);
}
builder.extend(wearableExtender);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
createAndroidNotificationChannel(context);
int notificationId = getNotificationId(habit);
notificationManager.notify(notificationId, notification);
try {
notificationManager.notify(notificationId, builder.build());
} catch(RuntimeException e) {
builder.setSound(null);
notificationManager.notify(notificationId, builder.build());
}
}
private boolean shouldShowReminderToday()
@@ -245,4 +259,19 @@ public class NotificationTray
return reminderDays[weekday];
}
}
public static void createAndroidNotificationChannel(Context context) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
if (SDK_INT >= Build.VERSION_CODES.O)
{
NotificationChannel channel =
new NotificationChannel(REMINDERS_CHANNEL_ID,
context.getResources().getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ import android.support.annotation.*;
import com.activeandroid.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.models.sqlite.records.*;
import java.io.*;
@@ -76,7 +77,16 @@ public abstract class DatabaseUtils
RepetitionRecord.class, ScoreRecord.class, StreakRecord.class)
.create();
ActiveAndroid.initialize(dbConfig);
try
{
ActiveAndroid.initialize(dbConfig);
}
catch (RuntimeException e)
{
if(e.getMessage().contains("downgrade"))
throw new InvalidDatabaseVersionException();
else throw e;
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")

View File

@@ -42,8 +42,7 @@ public abstract class DateUtils
public static long applyTimezone(long localTimestamp)
{
TimeZone tz = getTimezone();
long now = new Date(localTimestamp).getTime();
return now - tz.getOffset(now);
return localTimestamp - tz.getOffset(localTimestamp - tz.getOffset(localTimestamp));
}
public static String formatHeaderDate(GregorianCalendar day)
@@ -230,8 +229,7 @@ public abstract class DateUtils
public static long removeTimezone(long timestamp)
{
TimeZone tz = getTimezone();
long now = new Date(timestamp).getTime();
return now + tz.getOffset(now);
return timestamp + tz.getOffset(timestamp);
}
public static void setFixedLocalTime(Long timestamp)

View File

@@ -22,7 +22,9 @@ package org.isoron.uhabits.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.v4.view.*;
import android.util.*;
import android.view.*;
public abstract class InterfaceUtils
{
@@ -49,4 +51,10 @@ public abstract class InterfaceUtils
DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
}
public static boolean isLayoutRtl(View view)
{
return ViewCompat.getLayoutDirection(view) ==
ViewCompat.LAYOUT_DIRECTION_RTL;
}
}

View File

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

View File

@@ -146,6 +146,14 @@
style="@style/About.Item"
android:text="Angga Rifandi (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="raden20 (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="azzamsa (Bahasa Indonesia)"/>
<TextView
style="@style/About.Item"
android:text="David Nos (Català)"/>
@@ -178,6 +186,10 @@
style="@style/About.Item"
android:text="Can Altas (Deutsch)"/>
<TextView
style="@style/About.Item"
android:text="Laura Sophie (Deutsch)"/>
<TextView
style="@style/About.Item"
android:text="Ander Raso Vazquez (Español)"/>
@@ -186,6 +198,10 @@
style="@style/About.Item"
android:text="Beriain (Euskara)"/>
<TextView
style="@style/About.Item"
android:text="Osoitz (Euskara)"/>
<TextView
style="@style/About.Item"
android:text="Andreas Michelakis (Ελληνικά)"/>
@@ -198,6 +214,10 @@
style="@style/About.Item"
android:text="Saeed Esmaili (Fārsi)"/>
<TextView
style="@style/About.Item"
android:text="Behnood HRazy (Fārsi)"/>
<TextView
style="@style/About.Item"
android:text="François Mahé (Français)"/>
@@ -214,6 +234,10 @@
style="@style/About.Item"
android:text="Michael Faille (Français)"/>
<TextView
style="@style/About.Item"
android:text="Tiralka (Français)"/>
<TextView
style="@style/About.Item"
android:text="Ivan Krušlin (Hrvatski)"/>
@@ -230,6 +254,10 @@
style="@style/About.Item"
android:text="Jelle den Butter (Nederlands)"/>
<TextView
style="@style/About.Item"
android:text="nitovf9292 (Norsk)"/>
<TextView
style="@style/About.Item"
android:text="Adam Jurkiewicz (Polski)"/>
@@ -250,18 +278,26 @@
style="@style/About.Item"
android:text="Andrei Pleș (Română)"/>
<TextView
style="@style/About.Item"
android:text="Andreea Muscalagiu (Română)"/>
<TextView
style="@style/About.Item"
android:text="Dušan Strgar (Slovenščina)"/>
<TextView
style="@style/About.Item"
android:text="Dalecarlian (Svenska)"/>
android:text="Alexander Jansson (Svenska)"/>
<TextView
style="@style/About.Item"
android:text="Robin (Svenska)"/>
<TextView
style="@style/About.Item"
android:text="Sofia Veijonen (Suomen kieli)"/>
<TextView
style="@style/About.Item"
android:text="Đorđe Vasiljević (српски)"/>
@@ -270,6 +306,10 @@
style="@style/About.Item"
android:text="Caner Başaran (Türkçe)"/>
<TextView
style="@style/About.Item"
android:text="hodanli (Türkçe)"/>
<TextView
style="@style/About.Item"
android:text="Yurii Stavytskyi (Українська)"/>
@@ -278,6 +318,14 @@
style="@style/About.Item"
android:text="Rystard (Українська)"/>
<TextView
style="@style/About.Item"
android:text="Oglaigh Rystard (Українська)"/>
<TextView
style="@style/About.Item"
android:text="taras-ko (Українська)"/>
<TextView
style="@style/About.Item"
android:text="Limin Lu (中文)"/>
@@ -310,13 +358,62 @@
style="@style/About.Item"
android:text="Al Alloush (العَرَبِية‎)"/>
<TextView
style="@style/About.Item"
android:text="Boula (العَرَبِية‎)"/>
<TextView
style="@style/About.Item"
android:text="Israa Z (العَرَبِية‎)"/>
<TextView
style="@style/About.Item"
android:text="Josh Graham (한국어 )"/>
<TextView
style="@style/About.Item"
android:text="Seoyul (한국어 )"/>
<TextView
style="@style/About.Item"
android:text="Aman Satnami (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Niraj Yadav (हिन्दी)"/>
<TextView
style="@style/About.Item"
android:text="Yoav Argov (עברית‎)"/>
<TextView
style="@style/About.Item"
android:text="Mahdi Nasiri (فارسی‎)"/>
<TextView
style="@style/About.Item"
android:text="Mohammed Imthath (தமிழ்‎)"/>
<TextView
style="@style/About.Item"
android:text="magimai (தமிழ்‎)"/>
<TextView
style="@style/About.Item"
android:text="Anshoe (தமிழ்‎)"/>
<TextView
style="@style/About.Item"
android:text="Trần Thái (Tiếng Việt)"/>
<TextView
style="@style/About.Item"
android:text="Anh Quân (Tiếng Việt)"/>
<TextView
style="@style/About.Item"
android:text="pnhpnh (Tiếng Việt)"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -19,6 +19,34 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Herhaalde Gewoonte Boekhouer</string>
<string name="main_activity_title">Gewoontes</string>
<string name="action_settings">Instellings</string>
<string name="edit">Redigeer</string>
<string name="delete">Verwyder</string>
<string name="archive">Argiveer</string>
<string name="unarchive">Deargiveer</string>
<string name="add_habit">Voeg gewoonte by</string>
<string name="color_picker_default_title">Verander kleur</string>
<string name="toast_habit_created">Gewoonte geskep</string>
<string name="toast_habit_deleted">Gewoontes verwyder</string>
<string name="toast_habit_restored">Gewoontes herstel</string>
<string name="toast_nothing_to_undo">Niks om terug te doen nie</string>
<string name="toast_nothing_to_redo">Niks om oor te doen nie</string>
<string name="toast_habit_changed">Gewoonte verander</string>
<string name="toast_habit_changed_back">Gewoonte terug verander</string>
<string name="toast_habit_archived">Gewoontes geargiveer</string>
<string name="toast_habit_unarchived">Gewoontes gedeargiveer</string>
<string name="overview">Oorsig</string>
<!-- App introduction -->
<string name="intro_title_1">Welkom</string>
<string name="interval_15_minutes">15 minute</string>
<string name="interval_30_minutes">30 minute</string>
<string name="interval_1_hour">1 uur</string>
<string name="interval_2_hour">2 ure</string>
<string name="interval_4_hour">4 ure</string>
<string name="interval_8_hour">8 ure</string>
<string name="interval_24_hour">24 ure</string>
<string name="settings">Instellings</string>
<!-- Middle part of the sentence '1 time in xx days' -->
</resources>

View File

@@ -19,63 +19,63 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">لوب ملاحق العادة </string>
<string name="app_name">متعقب العادة لووب</string>
<string name="main_activity_title">عادات</string>
<string name="action_settings">إعدادات</string>
<string name="edit">تعديل</string>
<string name="delete">حذف</string>
<string name="archive">أرشيف</string>
<string name="archive">أرشفة</string>
<string name="unarchive">إزالة من الأرشيف</string>
<string name="add_habit">إضافة العادة</string>
<string name="color_picker_default_title">غير اللون</string>
<string name="toast_habit_created">تم صنع عادة </string>
<string name="toast_habit_deleted">تم حذف عادة </string>
<string name="toast_habit_restored">تم ترجيع عادة</string>
<string name="toast_nothing_to_undo">لا شيء للتراجع</string>
<string name="toast_nothing_to_redo">لا شيء لتكرار</string>
<string name="toast_habit_changed">تم تغييرعادة</string>
<string name="toast_habit_changed_back">تم ترجيع العادة إلى أصلها</string>
<string name="toast_habit_archived">تم أرشيف العادات</string>
<string name="toast_habit_unarchived">تم إزالة العادة من الأرشيف </string>
<string name="add_habit">إضافة عادة</string>
<string name="color_picker_default_title">تغيير اللون</string>
<string name="toast_habit_created">تم إنشاء عادة</string>
<string name="toast_habit_deleted">تم حذف العادات</string>
<string name="toast_habit_restored">تم إستعادة العادات</string>
<string name="toast_nothing_to_undo">لا شيء للألغاء</string>
<string name="toast_nothing_to_redo">لا شيء للإعادة</string>
<string name="toast_habit_changed">تم تغيير عادة</string>
<string name="toast_habit_changed_back">تم أرجاع العادة إلى أصلها</string>
<string name="toast_habit_archived">تم أرشفه العادات</string>
<string name="toast_habit_unarchived">تم الغاء ارشفه العادات</string>
<string name="overview">نظرة عامة</string>
<string name="habit_strength">قوة العادة</string>
<string name="history">التاريخ</string>
<string name="clear">مسح</string>
<string name="history">السجل</string>
<string name="clear">إزالة</string>
<string name="description_hint">السؤال (هل ... اليوم؟)</string>
<string name="repeat">كرر</string>
<string name="times_every">مرات في</string>
<string name="repeat">كرره</string>
<string name="times_every">مرات كل</string>
<string name="days">أيام</string>
<string name="reminder">تذكير</string>
<string name="discard">حذف</string>
<string name="reminder">التذكرة</string>
<string name="discard">تجاهل</string>
<string name="save">حفظ</string>
<string name="streaks">تقدم متتالية</string>
<string name="no_habits_found"> لا يوجد لديك عادات مفعله</string>
<string name="streaks">الانجازات</string>
<string name="no_habits_found">لا يوجد لديك عادات مفعلة</string>
<string name="long_press_to_toggle">أضغط و إستمر لتحقق أو ازل</string>
<string name="reminder_off">أوقف</string>
<string name="reminder_off">إيقاف</string>
<string name="validation_name_should_not_be_blank">لا يمكن أن يكون الإسم فارغ</string>
<string name="validation_number_should_be_positive">يجب أن يكون الرقم إيجابي</string>
<string name="validation_at_most_one_rep_per_day">يمكن أن يكون التكرار واحدة فقط كل يوم </string>
<string name="create_habit">اخلق عادة</string>
<string name="validation_number_should_be_positive">يجب أن يكون الرقم موجب.</string>
<string name="validation_at_most_one_rep_per_day">يجب أن يكون التكرار مرة واحدة فقط كل يوم</string>
<string name="create_habit">انشاء العادة</string>
<string name="edit_habit">تعديل العادة</string>
<string name="check">حقق</string>
<string name="snooze">لاحقا</string>
<string name="snooze">لاحقاً</string>
<!-- App introduction -->
<string name="intro_title_1">أهلا بك</string>
<string name="intro_description_1">لوب يساعدك على خلق والحفاظ على العادات الجيدة.</string>
<string name="intro_title_2">إنشاء بعض عادات جديدة</string>
<string name="intro_description_2">كل يوم، بعد أداء عادتك، وضع علامة على التطبيق.</string>
<string name="intro_description_1">لوب يساعدك في بدأ عادات جيدة والحفاظ عليها.</string>
<string name="intro_title_2">إنشاء عادات جديدة</string>
<string name="intro_description_2">كل يوم، بعد أداء عادتك، ضع علامة عليها في التطبيق.</string>
<string name="intro_title_3">حافظ على القيام بذلك</string>
<string name="intro_description_3">العادة المستمرة لفترات طويلة تكسب نجمة كامله</string>
<string name="intro_title_4">تتبع تقدمك</string>
<string name="intro_description_4">رسوم بيانية مفصلة تبين لكم كيف تحسن عاداتك مع مرور الوقت.</string>
<string name="intro_description_3">العادة المستمرة لفترة طويلة تكسب نجمة كامله.</string>
<string name="intro_title_4">تتبع اداءك</string>
<string name="intro_description_4">رسوم بيانية مفصلة تُريك كيف تحسنت عاداتك مع مرور الوقت.</string>
<string name="interval_15_minutes">15 دقيقة</string>
<string name="interval_30_minutes">30 دقيقة</string>
<string name="interval_1_hour">ساعة واحدة</string>
<string name="interval_2_hour">ساعتين</string>
<string name="interval_4_hour">أربع ساعات</string>
<string name="interval_8_hour">ثماني ساعات</string>
<string name="interval_24_hour">24 ساعة</string>
<string name="pref_toggle_title">تبديل بكبسه</string>
<string name="interval_2_hour">ساعتان</string>
<string name="interval_4_hour">٤ ساعات</string>
<string name="interval_8_hour">8 ساعات</string>
<string name="interval_24_hour">٢٤ ساعة</string>
<string name="pref_toggle_title">تبديل وضعية العادة بضغطة قصيرة</string>
<string name="pref_toggle_description">أكثر سهولة، لكنه ممكن يسبب كبسات غير مقصوده</string>
<string name="pref_snooze_interval_title">فترتي الغفوى على التذكير</string>
<string name="pref_rate_this_app">تقييم هذا التطبيق على جوجل بلاي</string>
@@ -92,6 +92,7 @@
<string name="hint_landscape">يمكنك ان ترى المزيد أيام عن طريق وضع الهاتف في وضع أفقي.</string>
<string name="delete_habits">حذف عادات</string>
<string name="delete_habits_message">سيتم حذف عادات بشكل دائم. هذا العمل لا يمكن التراجع عنه.</string>
<string name="habit_not_found">العادة حذفت/لم يتم العثور عليها</string>
<string name="weekends">عطلة نهاية الأسبوع</string>
<string name="any_weekday">أيام الأسبوع</string>
<string name="any_day">أي يوم</string>
@@ -156,7 +157,22 @@
<string name="score">النقاط</string>
<string name="reminder_sound">صوت تذكير</string>
<string name="none">صامت</string>
<string name="filter">تصنيف</string>
<string name="hide_completed">إخفاء المكتملة</string>
<string name="hide_archived">إخفاء المؤرشفة</string>
<string name="sticky_notifications">جعل الإشعارات ثابتة</string>
<string name="sticky_notifications_description">منع الإشعارات من تمريرها بعيداً.</string>
<string name="repair_database">إصلاح قاعدة البيانات</string>
<string name="database_repaired">تم إصلاح قاعدة البيانات.</string>
<string name="uncheck">إلغاء تحديد</string>
<string name="toggle">تبديل</string>
<string name="action">عمل</string>
<string name="habit">عادة</string>
<string name="sort">فرز</string>
<string name="manually">يدوياً</string>
<string name="by_name">حسب الإسم</string>
<string name="by_color">حسب اللون</string>
<string name="by_score">حسب النقاط</string>
<string name="download">تحميل</string>
<string name="export">استخراج</string>
</resources>

View File

@@ -37,7 +37,7 @@
<string name="toast_habit_changed_back">Промяната на навика е отменена.</string>
<string name="toast_habit_archived">Навиците са архивирани</string>
<string name="toast_habit_unarchived">Навиците са разархивирани</string>
<string name="overview">Обобщение</string>
<string name="overview">Обзор</string>
<string name="habit_strength">Сила на навика</string>
<string name="history">История</string>
<string name="clear">Изчистване</string>
@@ -57,7 +57,7 @@
<string name="validation_at_most_one_rep_per_day">Позволено е до едно повторение на ден.</string>
<string name="create_habit">Създаване на навик</string>
<string name="edit_habit">Редактиране на навик</string>
<string name="check">Потвърди</string>
<string name="check">Поставяне на отметка</string>
<string name="snooze">По-късно</string>
<!-- App introduction -->
<string name="intro_title_1">Добре дошли</string>
@@ -65,8 +65,8 @@
<string name="intro_title_2">Създайте нови навици</string>
<string name="intro_description_2">Всеки ден, след изпълнението на навика, поставете отметка в приложението.</string>
<string name="intro_title_3">Продължавайте да го изпълнявате</string>
<string name="intro_description_3">Навици изпълнявани редовно за дълго време ще получат пълна звезда.</string>
<string name="intro_title_4">Следете прогреса си</string>
<string name="intro_description_3">Навици изпълнявани редовно за дълго време ще ви спечелят пълна звезда.</string>
<string name="intro_title_4">Следете напредъка си</string>
<string name="intro_description_4">Подробни диаграми ви показват как вашите навици са се подобрили с времето.</string>
<string name="interval_15_minutes">15 минути</string>
<string name="interval_30_minutes">30 минути</string>
@@ -74,6 +74,7 @@
<string name="interval_2_hour">2 часа</string>
<string name="interval_4_hour">4 часа</string>
<string name="interval_8_hour">8 часа</string>
<string name="interval_24_hour">24 часа</string>
<string name="pref_toggle_title">Маркиране с кратко натискане</string>
<string name="pref_toggle_description">Поставяне на отметки с кратко натискане вместо с натискане и задържане. По-удобно, но може да доведе до неволно маркиране.</string>
<string name="pref_snooze_interval_title">Интервал на напомняне след отлагане</string>
@@ -91,6 +92,7 @@
<string name="hint_landscape">Може да виждате повече дни като обърнете телефона си в хоризонтално положение.</string>
<string name="delete_habits">Изтриване на навици</string>
<string name="delete_habits_message">Навиците ще се изтрият перманентно. Това действие не може да бъде отменено.</string>
<string name="habit_not_found">Навикът е изтрит / не е намерен</string>
<string name="weekends">Събота и неделя</string>
<string name="any_weekday">От понеделник до петък</string>
<string name="any_day">Всеки ден от седмицата</string>
@@ -100,7 +102,7 @@
<string name="clear_label">Изчистване</string>
<string name="select_hours">Избиране на час</string>
<string name="select_minutes">Избиране на минута</string>
<string name="about">Информация</string>
<string name="about">За приложението</string>
<string name="translators">Преводачи</string>
<string name="developers">Разработчици</string>
<string name="version_n">Версия %s</string>
@@ -146,6 +148,7 @@
<string name="month">Месец</string>
<string name="quarter">Тримесечие</string>
<string name="year">Година</string>
<string name="total">Общо</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">път в период от</string>
<string name="every_x_days">На всеки %d дни</string>
@@ -154,4 +157,22 @@
<string name="score">Сила</string>
<string name="reminder_sound">Звук за напомняне</string>
<string name="none">Няма</string>
<string name="filter">Филтър</string>
<string name="hide_completed">Скриване на завършените</string>
<string name="hide_archived">Скриване на архивираните</string>
<string name="sticky_notifications">Направи нотификациите постоянни</string>
<string name="sticky_notifications_description">Предотвратява изчистването на нотификацията с плъзване настрани.</string>
<string name="repair_database">Поправка на базата данни</string>
<string name="database_repaired">Базата данни е поправена.</string>
<string name="uncheck">Премахване на отметка</string>
<string name="toggle">Смяна</string>
<string name="action">Действие</string>
<string name="habit">Навик</string>
<string name="sort">Сортиране</string>
<string name="manually">Ръчно</string>
<string name="by_name">По име</string>
<string name="by_color">По цвят</string>
<string name="by_score">По сила</string>
<string name="download">Изтегляне</string>
<string name="export">Експортиране</string>
</resources>

View File

@@ -19,7 +19,7 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Sledování zvyků</string>
<string name="app_name">Loop Habit Tracker</string>
<string name="main_activity_title">Zvyky</string>
<string name="action_settings">Nastavení</string>
<string name="edit">Upravit</string>
@@ -45,11 +45,11 @@
<string name="repeat">Opakovat</string>
<string name="times_every">krát za</string>
<string name="days">dní</string>
<string name="reminder">Připomenout</string>
<string name="reminder">Připomenutí</string>
<string name="discard">Zrušit</string>
<string name="save">Uložit</string>
<string name="streaks">Serie</string>
<string name="no_habits_found">Nemáš žádné aktivní zvyky</string>
<string name="no_habits_found">Nemáte žádné nedokončené zvyky</string>
<string name="long_press_to_toggle">Stiskni a drž pro označení</string>
<string name="reminder_off">Vyp.</string>
<string name="validation_name_should_not_be_blank">Jméno musíte vyplnit.</string>
@@ -57,7 +57,7 @@
<string name="validation_at_most_one_rep_per_day">Můžete mít maximálně jedno označení denně.</string>
<string name="create_habit">Vytvořit zvyk</string>
<string name="edit_habit">Upravit zvyk</string>
<string name="check">Označeno</string>
<string name="check">Hotovo</string>
<string name="snooze">Odložit</string>
<!-- App introduction -->
<string name="intro_title_1">Vítejte</string>
@@ -91,13 +91,13 @@
<string name="hint_drag">Přeřazení záznamů proveď stisknutím a podržením názvu zvyku a poté přesunutím na správné místo.</string>
<string name="hint_landscape">Můžeš vidět více dnů otočením telefonu na šířku.</string>
<string name="delete_habits">Smazat zvyky</string>
<string name="delete_habits_message">Zvyky budou navždy odstraněny. Toto nelze vzít zpět.</string>
<string name="delete_habits_message">Označené zvyky budou navždy odstraněny. Toto nelze vzít zpět.</string>
<string name="habit_not_found">Zvyk smazán / nenalezen</string>
<string name="weekends">Víkendy</string>
<string name="any_weekday">Pondělí až pátek</string>
<string name="any_day">Jakýkoliv den v týdnu</string>
<string name="select_weekdays">Vyber dny</string>
<string name="export_to_csv">Exportuj CSV</string>
<string name="export_to_csv">Exportovat CSV</string>
<string name="done_label">Hotovo</string>
<string name="clear_label">Smazat</string>
<string name="select_hours">Vyber hodiny</string>
@@ -169,7 +169,7 @@
<string name="action">Akce</string>
<string name="habit">Zvyk</string>
<string name="sort">Řadit</string>
<string name="manually">Manuál</string>
<string name="manually">Ruč</string>
<string name="by_name">Abecedně</string>
<string name="by_color">Podle barvy</string>
<string name="by_score">Podle skóre</string>

View File

@@ -19,7 +19,7 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Loop Habit Tracker</string>
<string name="app_name">Loop \"Gewohnheiten Tracking\"</string>
<string name="main_activity_title">Gewohnheiten</string>
<string name="action_settings">Einstellungen</string>
<string name="edit">Bearbeiten</string>
@@ -48,26 +48,26 @@
<string name="reminder">Erinnerung</string>
<string name="discard">Verwerfen</string>
<string name="save">Speichern</string>
<string name="streaks">Strähnen</string>
<string name="streaks">Serien</string>
<string name="no_habits_found">Du hast keine aktiven Gewohnheiten</string>
<string name="long_press_to_toggle">Tippe und halte um aus- bzw. abzuwählen</string>
<string name="reminder_off">Aus</string>
<string name="validation_name_should_not_be_blank">Name darf nicht leer sein.</string>
<string name="validation_number_should_be_positive">Zahl muss positiv sein.</string>
<string name="validation_at_most_one_rep_per_day">Du musst wenigstens eine Wiederholung pro Tag haben</string>
<string name="validation_at_most_one_rep_per_day">Du kannst höchstens eine Wiederholung pro Tag haben</string>
<string name="create_habit">Gewohnheit erstellen</string>
<string name="edit_habit">Gewohnheit bearbeiten</string>
<string name="check">Abhaken</string>
<string name="check">Markieren</string>
<string name="snooze">Später</string>
<!-- App introduction -->
<string name="intro_title_1">Willkommen</string>
<string name="intro_description_1">Loop Habit Tracker hilft dir gute Gewohnheiten implementieren.</string>
<string name="intro_description_1">Loop Habit Tracker hilft dir dabei, gute Gewohnheiten anzunehmen.</string>
<string name="intro_title_2">Erstelle neue Gewohnheiten</string>
<string name="intro_description_2">Jeden Tag, nachdem du die Gewohnheit gemacht hast, hake sie in der App ab.</string>
<string name="intro_description_2">Hake die Gewohnheit jeden Tag in der App ab, nachdem du sie erledigt hast.</string>
<string name="intro_title_3">Bleib dran</string>
<string name="intro_description_3">Gewohnheiten, die über einen längeren Zeitraum absolviert werden, bekommen einen ganzen Stern.</string>
<string name="intro_title_4">Verfolge deinen Fortschritt</string>
<string name="intro_description_4">Detaillierte Diagramme zeigen dir an wie sich deine Gewohnheiten entwickelt haben.</string>
<string name="intro_description_4">Detaillierte Diagramme zeigen dir an, wie sich deine Gewohnheiten entwickelt haben.</string>
<string name="interval_15_minutes">15 Minuten</string>
<string name="interval_30_minutes">30 Minuten</string>
<string name="interval_1_hour">1 Stunde</string>
@@ -75,29 +75,29 @@
<string name="interval_4_hour">4 Stunden</string>
<string name="interval_8_hour">8 Stunden</string>
<string name="interval_24_hour">24 Stunden</string>
<string name="pref_toggle_title">Gewohnheiten durch kurzes Tippen abhaken</string>
<string name="pref_toggle_description">Abhaken durch einfaches Tippen, anstatt durch Tippen und Halten. Bequemer, kann aber eine falsche Auswahl verursachen.</string>
<string name="pref_toggle_title">Markierung durch kurzes Drücken ändern</string>
<string name="pref_toggle_description">Markierungen durch einfaches Tippen setzen anstatt durch Tippen und Halten. Bequemer, kann aber versehentlich eine Markierung ändern.</string>
<string name="pref_snooze_interval_title">\"Später erinnern\"-Intervall bei Erinnerungen</string>
<string name="pref_rate_this_app">Bewerte diese App auf Google Play</string>
<string name="pref_send_feedback">Sende dem Entwickler Feedback</string>
<string name="pref_view_source_code">Zeige den Quellcode auf GitHub</string>
<string name="pref_view_app_introduction">Zeige die Anleitung</string>
<string name="pref_view_app_introduction">Zeige die App-Einführung</string>
<string name="links">Links</string>
<string name="behavior">Verhalten</string>
<string name="name">Name</string>
<string name="settings">Einstellungen</string>
<string name="snooze_interval">\"Später erinnern\"-Intervall</string>
<string name="hint_title">Wusstest du?</string>
<string name="hint_drag">Um Einträge umzusortieren, tippe und ziehe sie an die richtige Stelle.</string>
<string name="hint_drag">Um Einträge umzusortieren, tippe, halte und ziehe sie an die richtige Stelle.</string>
<string name="hint_landscape">Du kannst mehr Tage sehen, wenn du dein Smartphone quer hältst.</string>
<string name="delete_habits">Gewohnheiten löschen</string>
<string name="delete_habits_message">Diese Gewohnheit wird permanent gelöscht. Dies kann nicht rückgängig gemacht werden.</string>
<string name="delete_habits">Lösche Gewohnheiten</string>
<string name="delete_habits_message">Die Gewohnheit wird für immer gelöscht. Dies kann nicht rückgängig gemacht werden.</string>
<string name="habit_not_found">Gewohnheit gelöscht / nicht gefunden</string>
<string name="weekends">An Wochenenden</string>
<string name="any_weekday">Werktags</string>
<string name="any_weekday">Montag bis Freitag</string>
<string name="any_day">Jeden Tag</string>
<string name="select_weekdays">Wähle die Tage</string>
<string name="export_to_csv">als CSV exportieren</string>
<string name="select_weekdays">Wähle Tage aus</string>
<string name="export_to_csv">Exportiere als CSV</string>
<string name="done_label">Fertig</string>
<string name="clear_label">Löschen</string>
<string name="select_hours">Wähle Stunden</string>
@@ -106,11 +106,11 @@
<string name="translators">Übersetzer</string>
<string name="developers">Entwickler</string>
<string name="version_n">Version %s</string>
<string name="frequency">Frequenz</string>
<string name="frequency">Häufigkeit</string>
<string name="checkmark">Häkchen</string>
<string name="strength">Stärke</string>
<string name="best_streaks">Beste Strähnen</string>
<string name="current_streaks">Derzeitige Strähne</string>
<string name="best_streaks">Beste Serien</string>
<string name="current_streaks">Derzeitige Serie</string>
<string name="number_of_repetitions">Anzahl der Wiederholungen</string>
<string name="last_x_days">Letzten %d Tage</string>
<string name="last_x_weeks">Letzten %d Wochen</string>
@@ -123,25 +123,25 @@
<string name="five_times_per_week">5 Mal pro Woche</string>
<string name="custom_frequency">Benutzerdefiniert</string>
<string name="help">Hilfe &amp; FAQ</string>
<string name="could_not_export">Der Export von Daten ist fehlgeschlagen.</string>
<string name="could_not_import">Der Import von Daten ist fehlgeschlagen.</string>
<string name="could_not_export">Fehler beim Exportieren der Daten.</string>
<string name="could_not_import">Fehler beim Importieren der Daten.</string>
<string name="file_not_recognized">Datei nicht erkannt.</string>
<string name="habits_imported">Gewohnheiten wurden erfolgreich importiert.</string>
<string name="habits_imported">Gewohnheiten erfolgreich importiert.</string>
<string name="full_backup_success">Vollständige Sicherung erfolgreich exportiert.</string>
<string name="import_data">Importiere Daten</string>
<string name="export_full_backup">Exportiere vollständige Sicherung</string>
<string name="import_data_summary">Unterstützt vollständige Sicherungen dieser App, sowie erstellte Sicherungen von Tickmate, HabitBull oder Rewire. Siehe FAQ für mehr Information.</string>
<string name="export_as_csv_summary">Erzeugt Dateien, die von Tabellenkalkulationsprogrammen, wie Microsoft Excel oder LibreOffice Calc, geöffnet werden können. Diese Dateien können nicht wieder importiert werden.</string>
<string name="export_full_backup_summary">Erzeugt eine Datei, die alle deine Daten enthält. Diese Datei kann wieder importiert werden.</string>
<string name="bug_report_failed">Fehlermeldung konnte nicht erstellt werden.</string>
<string name="generate_bug_report">Einen Fehler melden</string>
<string name="import_data_summary">Unterstützt vollständige Sicherungen dieser App sowie Sicherungen von Tickmate, HabitBull und Rewire. Siehe FAQ für weitere Informationen.</string>
<string name="export_as_csv_summary">Erstellt Dateien, die von Tabellenkalkulationsprogrammen wie Microsoft Excel oder LibreOffice Calc geöffnet werden können. Diese Dateien können nicht wieder importiert werden.</string>
<string name="export_full_backup_summary">Erstellt eine Datei, die alle deine Daten enthält. Diese Datei kann wieder importiert werden.</string>
<string name="bug_report_failed">Fehler beim Erstellen eines Fehlerberichts.</string>
<string name="generate_bug_report">Erstelle einen Fehlerbericht</string>
<string name="troubleshooting">Fehlerbehebung</string>
<string name="help_translate">Hilf diese App zu übersetzen</string>
<string name="help_translate">Hilf mit, diese App zu übersetzen</string>
<string name="night_mode">Nachtmodus</string>
<string name="use_pure_black">Reines Schwarz im Nachtmodus verwenden</string>
<string name="pure_black_description">Ersetzt das Grau im Hintergrund durch ein reines Schwarz im Nachtmodus. Reduziert den Stromverbrauch von Smartphones mit einem AMOLED Display.</string>
<string name="use_pure_black">Verwende reines Schwarz im Nachtmodus</string>
<string name="pure_black_description">Ersetzt im Nachtmodus das Grau im Hintergrund durch ein reines Schwarz. Reduziert den Stromverbrauch von Smartphones mit einem AMOLED Display.</string>
<string name="interface_preferences">Oberfläche</string>
<string name="reverse_days">Die Reihenfolge der Tage umkehren</string>
<string name="reverse_days">Kehre die Tagesreihenfolge um</string>
<string name="reverse_days_description">Zeigt die Tage im Hauptfenster in umgekehrter Reihenfolge an</string>
<string name="day">Tag</string>
<string name="week">Woche</string>
@@ -151,18 +151,18 @@
<string name="total">Gesamt</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">Mal in</string>
<string name="every_x_days">Jeden %d Tag</string>
<string name="every_x_weeks">Jede %d Woche</string>
<string name="every_x_months">Jeden %d Monat</string>
<string name="every_x_days">Alle %d Tage</string>
<string name="every_x_weeks">Alle %d Wochen</string>
<string name="every_x_months">Alle %d Monate</string>
<string name="score">Wertung</string>
<string name="reminder_sound">Benachrichtigungston</string>
<string name="none">Kein Ton</string>
<string name="reminder_sound">Erinnerungston</string>
<string name="none">Keiner</string>
<string name="filter">Filter</string>
<string name="hide_completed">Erledigte Gewohnheiten ausblenden</string>
<string name="hide_archived">Archivierte Gewohnheiten ausblenden</string>
<string name="sticky_notifications">Permanente Benachrichtigungen</string>
<string name="hide_completed">Abgeschlossene verbergen</string>
<string name="hide_archived">Archivierte verbergen</string>
<string name="sticky_notifications">Fixiere Benachrichtigungen</string>
<string name="sticky_notifications_description">Verhindert das Wegwischen von Benachrichtigungen.</string>
<string name="repair_database">Datenbank reparieren</string>
<string name="repair_database">Repariere Datenbank</string>
<string name="database_repaired">Datenbank repariert.</string>
<string name="uncheck">Abwählen</string>
<string name="toggle">Umschalten</string>
@@ -174,5 +174,5 @@
<string name="by_color">Nach Farbe</string>
<string name="by_score">Nach Wertung</string>
<string name="download">Runterladen</string>
<string name="export">Export</string>
<string name="export">Exportieren</string>
</resources>

View File

@@ -19,6 +19,76 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="main_activity_title">Kutimoj</string>
<string name="action_settings">Agordoj</string>
<string name="edit">Redakti</string>
<string name="delete">Forigi</string>
<string name="archive">Arĥivo</string>
<string name="unarchive">Elarĥivigi</string>
<string name="add_habit">Aldonu kutimon</string>
<string name="color_picker_default_title">Ŝanĝi koloron</string>
<string name="toast_habit_changed">Kutimo ŝanĝita</string>
<string name="toast_habit_archived">Kutimo arĥivita</string>
<string name="habit_strength">Kutimo forteco</string>
<string name="days">tagoj</string>
<string name="reminder">Memorigaĵoj</string>
<string name="discard">Nuligi</string>
<string name="save">Konservi</string>
<string name="streaks">Strioj</string>
<string name="reminder_off">Neaktiva</string>
<string name="snooze">Poste</string>
<!-- App introduction -->
<string name="intro_title_1">Bonvenon</string>
<string name="interval_15_minutes">15 minutoj</string>
<string name="interval_30_minutes">30 minutoj</string>
<string name="settings">Agordoj</string>
<string name="delete_habits">Forigi kutimojn</string>
<string name="weekends">Semajnfinoj</string>
<string name="any_weekday">Lundo al vendredo</string>
<string name="any_day">Io semajntago</string>
<string name="select_weekdays">Elekti tagojn</string>
<string name="export_to_csv">Eksporti kiel CSV</string>
<string name="done_label">Farite</string>
<string name="select_hours">Elekti horojn</string>
<string name="select_minutes">Elekti minutojn</string>
<string name="about">Pri programo</string>
<string name="translators">Tradukantoj</string>
<string name="developers">Evoluigantoj</string>
<string name="version_n">Versio %s</string>
<string name="frequency">Frekvenco</string>
<string name="strength">Forteco</string>
<string name="number_of_repetitions">Nombro de ripetoj</string>
<string name="last_x_days">Lastaj %d tagoj</string>
<string name="last_x_weeks">Lastaj %d semajnoj</string>
<string name="last_x_months">Lastaj %d monatoj</string>
<string name="last_x_years">Lastaj %d jaroj</string>
<string name="all_time">Ĉiuj tempoj</string>
<string name="every_day">Ĉiu tago</string>
<string name="every_week">Ĉiu semajno</string>
<string name="two_times_per_week">Dufoje en semajno</string>
<string name="five_times_per_week">Kvinfoje en semajno</string>
<string name="help">Helpo &amp; Ofte Demandite</string>
<string name="file_not_recognized">Dosiero ne rekonita.</string>
<string name="full_backup_success">Plena savkopio sukcese eksportita.</string>
<string name="troubleshooting">Problemserĉado</string>
<string name="night_mode">Nokta reĝimo</string>
<string name="day">Tago</string>
<string name="week">Semajno</string>
<string name="month">Monato</string>
<string name="quarter">Jarkvarono</string>
<string name="year">Jaro</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="none">Nenio</string>
<string name="filter">Filtrilo</string>
<string name="hide_completed">Kaŝi kompletajn</string>
<string name="hide_archived">Kaŝi arĥivitajn</string>
<string name="repair_database">Ripari datumbazon</string>
<string name="database_repaired">Datumbazon riparita.</string>
<string name="action">Ago</string>
<string name="habit">Kutimo</string>
<string name="sort">Enkursigi</string>
<string name="by_name">Laŭ nomo</string>
<string name="by_color">Laŭ koloro</string>
<string name="download">Elŝuti</string>
<string name="export">Eksporti</string>
</resources>

View File

@@ -28,16 +28,16 @@
<string name="unarchive">Desarchivar</string>
<string name="add_habit">Agregar hábito</string>
<string name="color_picker_default_title">Cambiar color</string>
<string name="toast_habit_created">Hábito creado.</string>
<string name="toast_habit_deleted">Hábitos eliminados.</string>
<string name="toast_habit_restored">Hábitos restaurados.</string>
<string name="toast_nothing_to_undo">Nada que deshacer.</string>
<string name="toast_nothing_to_redo">Nada que rehacer.</string>
<string name="toast_habit_changed">Hábito cambiado.</string>
<string name="toast_habit_changed_back">bito cambiado devuelta.</string>
<string name="toast_habit_archived">Hábitos archivados.</string>
<string name="toast_habit_unarchived">Hábitos desarchivados.</string>
<string name="overview">Visión general</string>
<string name="toast_habit_created">Hábito creado</string>
<string name="toast_habit_deleted">Hábitos eliminados</string>
<string name="toast_habit_restored">Hábitos restaurados</string>
<string name="toast_nothing_to_undo">Nada que deshacer</string>
<string name="toast_nothing_to_redo">Nada que rehacer</string>
<string name="toast_habit_changed">Hábito cambiado</string>
<string name="toast_habit_changed_back">Cambio en hábito vuelto atrás</string>
<string name="toast_habit_archived">Hábitos archivados</string>
<string name="toast_habit_unarchived">Hábitos desarchivados</string>
<string name="overview">Resumen</string>
<string name="habit_strength">Fuerza del hábito</string>
<string name="history">Historial</string>
<string name="clear">Borrar</string>
@@ -52,8 +52,8 @@
<string name="no_habits_found">No tienes hábitos activos</string>
<string name="long_press_to_toggle">Mantener apretado para marcar o desmarcar</string>
<string name="reminder_off">Apagado</string>
<string name="validation_name_should_not_be_blank">Nombre no puede estar en blanco.</string>
<string name="validation_number_should_be_positive">Número debe ser positivo.</string>
<string name="validation_name_should_not_be_blank">El nombre no puede quedar en blanco.</string>
<string name="validation_number_should_be_positive">El número debe ser positivo.</string>
<string name="validation_at_most_one_rep_per_day">Puedes tener como máximo una repetición por día</string>
<string name="create_habit">Crear hábito</string>
<string name="edit_habit">Editar hábito</string>
@@ -63,48 +63,51 @@
<string name="intro_title_1">Bienvenido</string>
<string name="intro_description_1">Loop Analizador de Hábitos te ayuda a crear y mantener buenos hábitos.</string>
<string name="intro_title_2">Crea algunos hábitos nuevos</string>
<string name="intro_description_2">Cada día, después de realizar tu hábito, pon una marca en la app.</string>
<string name="intro_title_3">Sigue haciéndolo.</string>
<string name="intro_description_2">Cada día, después de realizar tu hábito, pon una marca en la aplicación.</string>
<string name="intro_title_3">Sigue haciéndolo</string>
<string name="intro_description_3">Los hábitos realizados consistentemente por un largo tiempo ganarán una estrella completa.</string>
<string name="intro_title_4">Haz un seguimiento de tu progreso</string>
<string name="intro_description_4">Detallados gráficos muestran como han mejorado tus hábitos con el tiempo.</string>
<string name="intro_description_4">Gráficos detallados muestran cómo mejoraron sus hábitos con el tiempo.</string>
<string name="interval_15_minutes">15 minutos</string>
<string name="interval_30_minutes">30 minutos</string>
<string name="interval_1_hour">1 hora</string>
<string name="interval_2_hour">2 horas</string>
<string name="interval_4_hour">4 horas</string>
<string name="interval_8_hour">8 horas</string>
<string name="pref_toggle_title">Marca las repeticiones con una corta pulsación.</string>
<string name="interval_24_hour">24 horas</string>
<string name="pref_toggle_title">Marca las repeticiones con una pulsación corta</string>
<string name="pref_toggle_description">Más cómodo, pero puede causar marcas accidentales.</string>
<string name="pref_snooze_interval_title">Tiempo de espera al aplazar recordatorios.</string>
<string name="pref_rate_this_app">Valora esta app en Google Play</string>
<string name="pref_snooze_interval_title">Tiempo de espera al aplazar recordatorios</string>
<string name="pref_rate_this_app">Valora esta aplicación en Google Play</string>
<string name="pref_send_feedback">Enviar sugerencias al desarrollador</string>
<string name="pref_view_source_code">Ver código fuente en GitHub</string>
<string name="pref_view_app_introduction">Ver la introducción de la app</string>
<string name="pref_view_app_introduction">Ver la introducción de la aplicación</string>
<string name="links">Enlaces</string>
<string name="behavior">Comportamiento</string>
<string name="name">Nombre</string>
<string name="settings">Configuración</string>
<string name="snooze_interval">Intervalo de espera</string>
<string name="hint_title">¿Sabías qué?</string>
<string name="hint_drag">Para reordenar las entradas, mantén la pulsación sobre el nombre del hábito, después arrástralo a su posición correcta.</string>
<string name="hint_drag">Para reordenar las entradas, mantén la pulsado sobre el nombre del hábito, después arrástralo a su posición correcta.</string>
<string name="hint_landscape">Puedes ver más días al poner tu teléfono en modo horizontal.</string>
<string name="delete_habits">Eliminar Hábitos</string>
<string name="delete_habits_message">Los hábitos serán eliminados permanentemente. Esta acción no se puede deshacer.</string>
<string name="habit_not_found">Hábito eliminado / no encontrado</string>
<string name="weekends">Fines de semana</string>
<string name="any_weekday">Días laborables</string>
<string name="any_weekday">De lunes a viernes</string>
<string name="any_day">Cada día</string>
<string name="select_weekdays">Seleccionar días</string>
<string name="export_to_csv">Exportar datos (CSV)</string>
<string name="done_label">Hecho</string>
<string name="clear_label">Quitar</string>
<string name="select_hours">Seleccionar horas</string>
<string name="select_minutes">Seleccionar</string>
<string name="select_minutes">Seleccionar minutos</string>
<string name="about">Acerca de</string>
<string name="translators">Traductores</string>
<string name="developers">Desarrolladores</string>
<string name="version_n">Versión %s</string>
<string name="frequency">Frecuencia</string>
<string name="checkmark">Marca de verificación</string>
<string name="strength">Fuerza</string>
<string name="best_streaks">Mejores rachas</string>
<string name="current_streaks">Racha actual</string>
@@ -120,8 +123,8 @@
<string name="five_times_per_week">5 veces por semana</string>
<string name="custom_frequency">Personalizado...</string>
<string name="help">Ayuda &amp; FAQ</string>
<string name="could_not_export">Fallo al exportar datos.</string>
<string name="could_not_import">Fallo al importar datos.</string>
<string name="could_not_export">Error al exportar datos.</string>
<string name="could_not_import">Error al importar datos.</string>
<string name="file_not_recognized">Archivo no reconocido.</string>
<string name="habits_imported">Hábitos importados exitosamente.</string>
<string name="full_backup_success">Copia de seguridad exportada exitosamente.</string>
@@ -130,8 +133,8 @@
<string name="import_data_summary">Soporta exportar copias de seguridad completas, así como archivos generados por Tickmate, HabitBull o Rewire. Mira el FAQ para más información.</string>
<string name="export_as_csv_summary">Genera archivos que pueden ser abiertos por programas de hojas de cálculo como Microsoft Excel o OpenOffice Calc. Este archivo no puede volver a importarse.</string>
<string name="export_full_backup_summary">Genera un archivo que contiene todos tus datos. Este archivo puede volver a importarse.</string>
<string name="bug_report_failed">Fallo al generar el informe del bug.</string>
<string name="generate_bug_report">Generar informe de bug</string>
<string name="bug_report_failed">Error al generar el reporte de error.</string>
<string name="generate_bug_report">Generar reporte de errores</string>
<string name="troubleshooting">Solución de problemas</string>
<string name="help_translate">Ayuda a traducir esta app</string>
<string name="night_mode">Modo nocturno</string>
@@ -145,6 +148,7 @@
<string name="month">Mes</string>
<string name="quarter">Cuatrimestre</string>
<string name="year">Año</string>
<string name="total">Total</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">veces en</string>
<string name="every_x_days">Cada %d días</string>
@@ -153,4 +157,22 @@
<string name="score">Puntuación</string>
<string name="reminder_sound">Sonido de recordatorio</string>
<string name="none">Ninguno</string>
<string name="filter">Filtrar</string>
<string name="hide_completed">Ocultar completos</string>
<string name="hide_archived">Ocultar archivados</string>
<string name="sticky_notifications">Hacer notificaciones fijas</string>
<string name="sticky_notifications_description">Evita que las notificaciones sean descartadas.</string>
<string name="repair_database">Reparar base de datos</string>
<string name="database_repaired">Base de datos reparada.</string>
<string name="uncheck">Desmarcar</string>
<string name="toggle">Alternar</string>
<string name="action">Acción</string>
<string name="habit">Hábito</string>
<string name="sort">Ordenar</string>
<string name="manually">Manualmente</string>
<string name="by_name">Por nombre</string>
<string name="by_color">Por color</string>
<string name="by_score">Por puntuación</string>
<string name="download">Descargar</string>
<string name="export">Exportar</string>
</resources>

View File

@@ -19,24 +19,26 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Loop Habit Tracker</string>
<string name="main_activity_title">Ohiturak</string>
<string name="action_settings">Ezarpenak</string>
<string name="edit">Editatu</string>
<string name="delete">Ezabatu</string>
<string name="archive">Artxibatu</string>
<string name="unarchive">Ezartxibatu</string>
<string name="add_habit">Ohitura gehitu</string>
<string name="unarchive">Desartxibatu</string>
<string name="add_habit">Gehitu ohitura</string>
<string name="color_picker_default_title">Kolorea aldatu</string>
<string name="toast_habit_created">Ohitura sortu da.</string>
<string name="toast_habit_deleted">Ohiturak ezabatu dira.</string>
<string name="toast_habit_restored">Ohiturak berrezarri dira.</string>
<string name="toast_nothing_to_undo">Ez dago desegiteko ezer.</string>
<string name="toast_nothing_to_redo">Ez dago berregiteko ezer.</string>
<string name="toast_habit_changed">Ohitura aldatu da.</string>
<string name="toast_habit_changed_back">Ohitura lehengoratu da.</string>
<string name="toast_habit_archived">Ohiturak artxibatu dira.</string>
<string name="toast_habit_unarchived">Ohiturak ezartxibatu dira.</string>
<string name="toast_habit_created">Ohitura sortu da</string>
<string name="toast_habit_deleted">Ohiturak ezabatu dira</string>
<string name="toast_habit_restored">Ohiturak berrezarri dira</string>
<string name="toast_nothing_to_undo">Ez dago ezer desegiteko</string>
<string name="toast_nothing_to_redo">Ez dago ezer berregiteko</string>
<string name="toast_habit_changed">Ohitura aldatu egin da</string>
<string name="toast_habit_changed_back">Ohitura berrezarri da</string>
<string name="toast_habit_archived">Ohiturak artxibatu dira</string>
<string name="toast_habit_unarchived">Ohiturak desartxibatu dira</string>
<string name="overview">Ikuspegi orokorra</string>
<string name="habit_strength">Ohituraren indarra</string>
<string name="history">Historia</string>
<string name="clear">Garbitu</string>
<string name="description_hint">Galdera (... al duzu gaur?)</string>
@@ -46,6 +48,7 @@
<string name="reminder">Oroigarria</string>
<string name="discard">Baztertu</string>
<string name="save">Gorde</string>
<string name="streaks">Boladak</string>
<string name="no_habits_found">Ez duzu ohitura aktiborik</string>
<string name="long_press_to_toggle">Sakatu eta mantendu markatu edo desmarkatzeko</string>
<string name="reminder_off">Itzalita</string>
@@ -54,12 +57,15 @@
<string name="validation_at_most_one_rep_per_day">Gehienez errepikapen bat eguneko izan dezakezu</string>
<string name="create_habit">Ohitura sortu</string>
<string name="edit_habit">Ohitura editatu</string>
<string name="snooze">Beranduago</string>
<string name="check">Markatu</string>
<string name="snooze">Geroago</string>
<!-- App introduction -->
<string name="intro_title_1">Ongi etorri</string>
<string name="intro_description_1">Loop Habit Tracker-ek ohitura onak hartzen eta mantentzen laguntzen dizu.</string>
<string name="intro_title_2">Sor itzazu ohitura berri batzuk</string>
<string name="intro_description_2">Egunero, zure ohitura egin ostean, jarri ezazu egiaztatze marka bat aplikazioan.</string>
<string name="intro_title_3">Jarrai ezazu ohitura egiten</string>
<string name="intro_description_3">Denbora luzean zehar trinkotasunez egindako ohiturek izar oso bat irabaziko dute.</string>
<string name="intro_title_4">Jarrai ezazu zure aurrerapena</string>
<string name="intro_description_4">Grafiko zehatzen bitartez denboran zehar zure ohiturak nola hobetu diren ikus ditzakezu</string>
<string name="interval_15_minutes">15 minutu</string>
@@ -68,6 +74,10 @@
<string name="interval_2_hour">2 ordu</string>
<string name="interval_4_hour">4 ordu</string>
<string name="interval_8_hour">8 ordu</string>
<string name="interval_24_hour">24 ordu</string>
<string name="pref_toggle_title">Ukitze laburrarekin markatu</string>
<string name="pref_toggle_description">Ukitze bakar batekin marka jartzen du ukitu eta mantendu egin beharrean. Erosoagoa, baina nahi gabeko markak ekar litzake.</string>
<string name="pref_snooze_interval_title">Atzeratze tartea oroigarrietan</string>
<string name="pref_rate_this_app">Aplikazio hau Google Playen puntuatu</string>
<string name="pref_send_feedback">Zure iritzia garatzaileari bidali</string>
<string name="pref_view_source_code">Iturburu kodea GitHuben ikusi</string>
@@ -76,11 +86,13 @@
<string name="behavior">Portaera</string>
<string name="name">Izena</string>
<string name="settings">Ezarpenak</string>
<string name="snooze_interval">Atzeratze tartea</string>
<string name="hint_title">Ba al zenekien?</string>
<string name="hint_drag">Sarrerak berrantolatzeko, sakatu eta mantendu ohituraren izena, ondoren mugi ezazu leku aproposera.</string>
<string name="hint_landscape">Egun gehiago ikus ditzakezu zure gailua paisai moduan jarriz.</string>
<string name="delete_habits">Ohiturak ezabatu</string>
<string name="delete_habits_message">Ohiturak betirako ezabatuko dira. Ekintza hau ezin da desegin.</string>
<string name="habit_not_found">Ohitura ezabatua / ez aurkitua</string>
<string name="weekends">Asteburuak</string>
<string name="any_weekday">Astelehenetik ostiralera</string>
<string name="any_day">Astearen edozen egun</string>
@@ -96,6 +108,9 @@
<string name="version_n">%s bertsioa</string>
<string name="frequency">Maiztasuna</string>
<string name="checkmark">Egiaztatze marka</string>
<string name="strength">Indarra</string>
<string name="best_streaks">Bolada onenak</string>
<string name="current_streaks">Uneko bolada</string>
<string name="number_of_repetitions">Errepikapenen kopurua</string>
<string name="last_x_days">Azken %d egunak</string>
<string name="last_x_weeks">Azken %d asteak</string>
@@ -115,18 +130,49 @@
<string name="full_backup_success">Babes kopia osoa ondo esportatu da.</string>
<string name="import_data">Datuak inportatu</string>
<string name="export_full_backup">Babes kopia osoa esportatu</string>
<string name="import_data_summary">Aplikazio honek esportatutako babes kopia osoak onartzen dira, baita Tickmate, HabitBull edo Rewirek sortutako fitxategiak ere. Ikusi ohito galderak informazio gehiago lortzeko.</string>
<string name="export_as_csv_summary">Microsoft Excel edo OpenOffice Calc bezalako kalkulu orrietarako softwareak ireki dezaketen fitxategiak sortzen ditu. Fitxategi hau ezin da berriz inportatu.</string>
<string name="export_full_backup_summary">Zure datu guztiak dituen fitxategi bat sortzen du. Fitxategi hau ezin da berriz inportatu.</string>
<string name="bug_report_failed">Huts akats txostena sortzerakoan.</string>
<string name="generate_bug_report">Akats txostena sortu</string>
<string name="troubleshooting">Arazoen konponketa</string>
<string name="help_translate">Lagundu aplikazio hau itzultzen</string>
<string name="night_mode">Gau modua</string>
<string name="use_pure_black">Benetazko beltza erabili gau moduan</string>
<string name="pure_black_description">Atzeko plano grisak beltz hutsez aldatzen ditu gau moduan. Bateriaren erabilera gutxitzen du AMOLED duten gailuetan.</string>
<string name="interface_preferences">Interfazea</string>
<string name="reverse_days">Egunak atzekoz aurrera ordenatu</string>
<string name="reverse_days_description">Pantaila nagusian egunak atzekoz aurrera ikusi</string>
<string name="day">Eguna</string>
<string name="week">Astea</string>
<string name="month">Hilabetea</string>
<string name="quarter">Hiruhilekoa</string>
<string name="year">Urtea</string>
<string name="total">Guztira</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">denbora</string>
<string name="every_x_days">%d egunero</string>
<string name="every_x_weeks">%d astero</string>
<string name="every_x_months">%d hilabetero</string>
<string name="score">Puntuak</string>
<string name="reminder_sound">Oroigarriaren soinua</string>
<string name="none">Bat ere ez</string>
<string name="filter">Iragazkia</string>
<string name="hide_completed">Ezkutatu lortutakoak</string>
<string name="hide_archived">Artxibatutakoak ezkutatu</string>
<string name="sticky_notifications">Jakinarazpenak itsaskorrak bihurtu</string>
<string name="sticky_notifications_description">Jakinarazpenak keinu batez ezabatzea sahiesten du.</string>
<string name="repair_database">Datu basea konpondu</string>
<string name="database_repaired">Datu basea konpondu da.</string>
<string name="uncheck">Desmarkatu</string>
<string name="toggle">Aldatu</string>
<string name="action">Ekintza</string>
<string name="habit">Ohitura</string>
<string name="sort">Ordenatu</string>
<string name="manually">Eskuz</string>
<string name="by_name">Izenaren arabera</string>
<string name="by_color">Kolorearen arabera</string>
<string name="by_score">Puntuen arabera</string>
<string name="download">Deskargatu</string>
<string name="export">Esportatu</string>
</resources>

View File

@@ -19,34 +19,34 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">عادت‌سنج</string>
<string name="app_name">Loop Habit Tracker</string>
<string name="main_activity_title">عادت‌ها</string>
<string name="action_settings">تنظیمات</string>
<string name="edit">ویرایش</string>
<string name="delete">حذف</string>
<string name="archive">آرشیو کردن</string>
<string name="unarchive">خارج کردن از آرشیو</string>
<string name="archive">بایگانی کن</string>
<string name="unarchive">خارج کردن از بایگانی</string>
<string name="add_habit">افزودن عادت</string>
<string name="color_picker_default_title">تغییر رنگ</string>
<string name="toast_habit_created">عادت ساخته شد.</string>
<string name="toast_habit_deleted">عادت حذف شد.</string>
<string name="toast_habit_restored">عادت بازگردانده شد.</string>
<string name="toast_nothing_to_undo">چیزی برای واکردن وجود ندارد.</string>
<string name="toast_nothing_to_redo">چیزی برای برگشت وجود ندارد.</string>
<string name="toast_habit_created">عادت ایجاد شد</string>
<string name="toast_habit_deleted">عادت حذف شد</string>
<string name="toast_habit_restored">عادت بازگردانده شد</string>
<string name="toast_nothing_to_undo">چیزی برای بازگرداندن به حالت قبلی وجود ندارد</string>
<string name="toast_nothing_to_redo">چیزی برای انجام مجدد وجود ندارد</string>
<string name="toast_habit_changed">عادت تغییر کرد.</string>
<string name="toast_habit_changed_back">عادت به حالت قبل برگشت.</string>
<string name="toast_habit_archived">عادت آرشیو شد.</string>
<string name="toast_habit_unarchived">عادت از آرشیو خارج شد.</string>
<string name="toast_habit_changed_back">عادت به حالت قبل برگشت</string>
<string name="toast_habit_archived">عادتها بایگانی شدند</string>
<string name="toast_habit_unarchived">عادتها از بایگانی خارج شدند</string>
<string name="overview">مرور</string>
<string name="habit_strength">قوت عادت</string>
<string name="habit_strength">قدرت عادت</string>
<string name="history">تاریخچه</string>
<string name="clear">حذف</string>
<string name="clear">بی‌خیال</string>
<string name="description_hint">سوال (آیا امروز شما&#8230;؟)</string>
<string name="repeat">تکرار</string>
<string name="times_every">بار در هر</string>
<string name="days">روزها</string>
<string name="days">روز</string>
<string name="reminder">یادآور</string>
<string name="discard">حذف تغییرات</string>
<string name="discard">بی‌خیال</string>
<string name="save">ذخیره</string>
<string name="streaks">روزهای پیوسته</string>
<string name="no_habits_found">شما هیچ عادت فعالی ندارید</string>
@@ -55,18 +55,18 @@
<string name="validation_name_should_not_be_blank">جای اسم نمی‌تواند خالی باشد.</string>
<string name="validation_number_should_be_positive">عدد بایستی مثبت باشد.</string>
<string name="validation_at_most_one_rep_per_day">شما در نهایت می‌توانید یک تکرار در یک روز داشته باشید.</string>
<string name="create_habit">ساخت عادت</string>
<string name="create_habit">درج عادت جدید</string>
<string name="edit_habit">ویرایش عادت</string>
<string name="check">تیک زدن</string>
<string name="snooze">بعدا</string>
<string name="snooze">بعداً</string>
<!-- App introduction -->
<string name="intro_title_1">خوش آمدید</string>
<string name="intro_description_1">رهگیر عادت به شما کمک می‌کند تا برای خودتان عادت‌های خوبی بسازید.</string>
<string name="intro_description_1">رهگیر عادت لوپ به شما کمک می‌کند تا برای خودتان عادت‌های خوبی بسازید.</string>
<string name="intro_title_2">ساخت چند عادت جدید</string>
<string name="intro_description_2">هر روز، بعد از اینکه کار مربوط به عادت را انجام دادید، تیک مربوط به آن در برنامه را بزنید.</string>
<string name="intro_description_2">هر روز، بعد از انجام عادت، آن را در برنامه تیک بزنید.</string>
<string name="intro_title_3">ادامه دهید</string>
<string name="intro_description_3">عادت‌هایی که به صورت پیوسته برای مدت طولانی انجام شده‌اند یک ستاره‌ی کامل دریافت می‌کنند.</string>
<string name="intro_title_4">پشرفت خود را رهگیری کنید</string>
<string name="intro_title_4">پیشرفت خود را رهگیری کنید</string>
<string name="intro_description_4">نمودار جزئیات به شما نشان می‌دهد که چطور عادت‌هایتان با گذشت زمان بهبود پیدا کرده‌اند.</string>
<string name="interval_15_minutes">۱۵ دقیقه</string>
<string name="interval_30_minutes">۳۰ دقیقه</string>
@@ -74,30 +74,40 @@
<string name="interval_2_hour">۲ ساعت</string>
<string name="interval_4_hour">۴ ساعت</string>
<string name="interval_8_hour">۸ ساعت</string>
<string name="interval_24_hour">۲۴ ساعت</string>
<string name="pref_toggle_title">با اشاره‌ی کوتاه‌مدت وضعیت عادت را تغییر بده</string>
<string name="pref_toggle_description">راحت‌تر است ولی ممکن است باعث شود اشتباهی عادتی را تیک بزنید.</string>
<string name="pref_rate_this_app">دادن امتیاز به اپ در گوگل‌پلی</string>
<string name="pref_toggle_description">تیک زدن با تک‌ضربه در مقابل ضربه‌زدن و نگه‌داشتن راحت‌تر است ولی ممکن است باعث شود اشتباهی عادتی را تیک بزنید.</string>
<string name="pref_snooze_interval_title">بازه به تعویق انداختن یادآورها</string>
<string name="pref_rate_this_app">به این برنامه در گوگل‌پلی امتیاز بدهید</string>
<string name="pref_send_feedback">ارسال بازخورد به توسعه‌دهنده</string>
<string name="pref_view_source_code">دیدن سورس اپ در گیت‌هاب</string>
<string name="pref_view_app_introduction">دیدن معرفی اپ</string>
<string name="pref_view_source_code">دیدن منبع برنامه در گیت‌هاب</string>
<string name="pref_view_app_introduction">مشاهده معرفی برنامه</string>
<string name="links">لینک‌ها</string>
<string name="behavior">رفتار</string>
<string name="name">نام</string>
<string name="settings">تنظیمات</string>
<string name="hint_title">آیا می‌دانید؟</string>
<string name="snooze_interval">بازه به تعویق انداختن</string>
<string name="hint_title">آیا می دانستید؟</string>
<string name="hint_drag">برای جابجایی عناوین، انگشتتان را روی نام عادت مورد نظر بگذارید و نگه دارید، سپس آن را به محل صحیح بکشید.</string>
<string name="hint_landscape">با قرار دادن گوشی در حالت افقی می‌توانید روزهای بیشتری را ببینید.</string>
<string name="delete_habits">حذف عادت‌ها</string>
<string name="delete_habits_message">عادت‌ها برای همیشه حذف خواهد شد. این عمل قابل بازگشت نیست.</string>
<string name="habit_not_found">عادت حذف شده/ پیدا نشد</string>
<string name="weekends">آخر هفته‌ها</string>
<string name="any_weekday">دوشنبه تا جمعه</string>
<string name="any_day">هر روز از هفته</string>
<string name="any_day">هر روز هفته</string>
<string name="select_weekdays">انتخاب روزها</string>
<string name="export_to_csv">دخیره به عنوان فایل CSV</string>
<string name="done_label">انجام شده</string>
<string name="clear_label">حذف</string>
<string name="select_hours">انتخاب ساعتها</string>
<string name="export_to_csv">صدور فایل CSV</string>
<string name="done_label">انجام شد</string>
<string name="clear_label">بی‌خیال</string>
<string name="select_hours">انتخاب ساعت</string>
<string name="select_minutes">انتخاب دقیقه</string>
<string name="about">درباره</string>
<string name="translators">مترجمین</string>
<string name="translators">مترجمان</string>
<string name="developers">توسعه‌دهندگان</string>
<string name="version_n">نسخه %s</string>
<string name="frequency">تناوب</string>
<string name="checkmark">علامت</string>
<string name="strength">قدرت</string>
<string name="best_streaks">بهترین استمرار</string>
<string name="current_streaks">استمرار فعلی</string>
@@ -106,20 +116,39 @@
<string name="last_x_weeks">%d هفته اخیر</string>
<string name="last_x_months">%d ماه اخیر</string>
<string name="last_x_years">%d سال اخیر</string>
<string name="all_time">همیشه</string>
<string name="every_day">هر روز</string>
<string name="every_week">هر هفته</string>
<string name="two_times_per_week">۲ بار در هفته</string>
<string name="five_times_per_week">۵ بار در هفته</string>
<string name="custom_frequency">سفارشی‌سازی ...</string>
<string name="help">راهنما و سوالات متداول</string>
<string name="could_not_export">خطا در صدور اطلاعات.</string>
<string name="could_not_import">خطا در وارد کردن اطلاعات.</string>
<string name="file_not_recognized">پرونده شناخته شده نیست.</string>
<string name="habits_imported">عادت‌ها با موفقیت وارد شدند.</string>
<string name="full_backup_success">پرونده پشتیبان کامل، با موفقیت صادر شد.</string>
<string name="import_data">ورود اطلاعات</string>
<string name="export_full_backup">پشتیبان گیری کامل</string>
<string name="import_data_summary">علاوه بر پشتیبان کامل تهیه شده توسط این برنامه، از پرونده‌های تولید شده توسط Tickmate، HabitbBull و یا Rewire هم پشتیبانی می‌شود. برای اطلاعات بیشتر سوالات متداول را ببینید.</string>
<string name="export_as_csv_summary">پرونده‌ای تولید می‌کند که می‌توان توسط برنامه‌های صفحه گسترده مانند Microsoft Excel و یا OpenOffice Calc بازشان کرد. این پرونده قابلیت وارد کردن مجدد به این برنامه را ندارد.</string>
<string name="export_full_backup_summary">پرونده‌ای تولید می‌کند که شامل تمام اطلاعات شما است. این پرونده قابل بازیابی توسط این برنامه می‌باشد.</string>
<string name="bug_report_failed">خطایی در تولید گزارش مشکلات بوجود آمد.</string>
<string name="generate_bug_report">ایجاد گزارش مشکلات</string>
<string name="troubleshooting">ایرادیابی</string>
<string name="night_mode">حالت شبانه</string>
<string name="help_translate">کمک برای ترجمه این برنامه</string>
<string name="night_mode">حالت شب</string>
<string name="use_pure_black">استفاده از رنگ سیاه خالص در حالت شبانه</string>
<string name="pure_black_description">جایگزینی پس زمینه خاکستری با سیاه خالص در حالت شب. استفاده از باتری در گوشی‌های با صفحه نمایش AMOLED را کاهش می‌دهد.</string>
<string name="interface_preferences">رابط کاربری</string>
<string name="reverse_days">معکوس کردن ترتیب روزها</string>
<string name="reverse_days_description">روزها را در صفحه اصلی با ترتیب معکوس نمایش می‌دهد</string>
<string name="day">روز</string>
<string name="week">هفته</string>
<string name="month">ماه</string>
<string name="quarter">فصل</string>
<string name="year">سال</string>
<string name="total">مجموع</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">بار در هر</string>
<string name="every_x_days">هر %d روز یک‌بار</string>
@@ -128,4 +157,22 @@
<string name="score">امتیاز</string>
<string name="reminder_sound">صدای یادآور</string>
<string name="none">هیچ‌کدام</string>
<string name="filter">فیلتر</string>
<string name="hide_completed">مخفی کردن کامل شده‌ها</string>
<string name="hide_archived">مخفی کردن بایگانی شده‌ها</string>
<string name="sticky_notifications">چسبناک کردن اعلان‌ها</string>
<string name="sticky_notifications_description">از رد کردن اعلان با کشیدن جلوگیری می‌کند.</string>
<string name="repair_database">تعمیر پایگاه داده</string>
<string name="database_repaired">پایگاه داده تعمیر شد.</string>
<string name="uncheck">برداشتن تیک</string>
<string name="toggle">تغییر وضعیت</string>
<string name="action">اقدام</string>
<string name="habit">عادت</string>
<string name="sort">مرتب‌سازی</string>
<string name="manually">دستی</string>
<string name="by_name">بر اساس نام</string>
<string name="by_color">بر اساس رنگ</string>
<string name="by_score">بر اساس امتیاز</string>
<string name="download">بارگيری</string>
<string name="export">صدور</string>
</resources>

View File

@@ -19,6 +19,65 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">Rutiini - Tracker</string>
<string name="main_activity_title">Rutiinit</string>
<string name="action_settings">Asetukset</string>
<string name="edit">Muokkaa</string>
<string name="delete">Poista</string>
<string name="archive">Arkistoi</string>
<string name="add_habit">Lisää rutiini</string>
<string name="color_picker_default_title">Vaihda väriä</string>
<string name="toast_habit_created">Rutiini luotu</string>
<string name="toast_habit_deleted">Rutiinit poistettu</string>
<string name="toast_habit_restored">Rutiinit palautettu</string>
<string name="toast_habit_changed">Rutiini muutettu</string>
<string name="toast_habit_changed_back">Rutiini muutettu takaisin</string>
<string name="overview">Yleiskatsaus</string>
<string name="history">Historia</string>
<string name="clear">Tyhjennä</string>
<string name="description_hint">Kysymys (Teitkö... tänään?)</string>
<string name="repeat">Toista</string>
<string name="times_every">kertaa</string>
<string name="days">päivässä</string>
<string name="reminder">Muistutus</string>
<string name="discard">Hylkää</string>
<string name="save">Tallenna</string>
<string name="streaks">Pisimmät toistot</string>
<string name="no_habits_found">Ei aktiivisia rutiineja</string>
<string name="long_press_to_toggle">Paina pitkään merkitäksesi suoritetuksi tai postaaksesi suorituksen</string>
<string name="reminder_off">Pois päältä</string>
<string name="validation_name_should_not_be_blank">Nimi ei voi olla tyhjä.</string>
<string name="validation_number_should_be_positive">Luvun on oltava positiivinen.</string>
<string name="create_habit">Luo rutiini</string>
<string name="edit_habit">Muokkaa rutiinia</string>
<string name="check">Tehty</string>
<string name="snooze">Lykkää</string>
<!-- App introduction -->
<string name="intro_title_1">Tervetuloa</string>
<string name="intro_title_2">Merkitse uusia rutiineja</string>
<string name="intro_description_2">Joka päivä, suoritettuasi rutiinin, merkitse se sovellukseen.</string>
<string name="links">Linkit</string>
<string name="behavior">Käyttäytyminen</string>
<string name="name">Nimi</string>
<string name="settings">Asetukset</string>
<string name="hint_title">Tiesitkö?</string>
<string name="done_label">Valmis</string>
<string name="clear_label">Tyhjennä</string>
<string name="translators">Kääntäjät</string>
<string name="developers">Kehittäjät</string>
<string name="version_n">Versio %s</string>
<string name="every_day">Joka päivä</string>
<string name="every_week">Joka viikko</string>
<string name="two_times_per_week">2 kertaa viikossa</string>
<string name="five_times_per_week">5 kertaa viikossa</string>
<string name="custom_frequency">Mukautettu&#8230;</string>
<string name="night_mode">Yötila</string>
<string name="use_pure_black">Käytä puhdasta mustaa yötilassa</string>
<string name="day">Päivä</string>
<string name="week">Viikko</string>
<string name="month">Kuukausi</string>
<string name="quarter">Kvartaali</string>
<string name="year">Vuosi</string>
<string name="total">Yhteensä</string>
<!-- Middle part of the sentence '1 time in xx days' -->
</resources>

View File

@@ -28,20 +28,20 @@
<string name="unarchive">Désarchiver</string>
<string name="add_habit">Ajouter une habitude</string>
<string name="color_picker_default_title">Changer la couleur</string>
<string name="toast_habit_created">Habitude créée.</string>
<string name="toast_habit_deleted">Habitude supprimée.</string>
<string name="toast_habit_restored">Habitude rétablie.</string>
<string name="toast_nothing_to_undo">Rien à annuler.</string>
<string name="toast_nothing_to_redo">Rien à refaire.</string>
<string name="toast_habit_changed">Habitude changée.</string>
<string name="toast_habit_changed_back">Habitude restaurée.</string>
<string name="toast_habit_archived">Habitudes archivées.</string>
<string name="toast_habit_unarchived">Habitudes désarchivées.</string>
<string name="toast_habit_created">Habitude créée</string>
<string name="toast_habit_deleted">Habitudes supprimées</string>
<string name="toast_habit_restored">Habitudes rétablies</string>
<string name="toast_nothing_to_undo">Rien à annuler</string>
<string name="toast_nothing_to_redo">Rien à refaire</string>
<string name="toast_habit_changed">Habitude changée</string>
<string name="toast_habit_changed_back">Habitude restaurée</string>
<string name="toast_habit_archived">Habitudes archivées</string>
<string name="toast_habit_unarchived">Habitudes désarchivées</string>
<string name="overview">Vue d\'ensemble</string>
<string name="habit_strength">Force de l\'habitude</string>
<string name="history">Historique</string>
<string name="clear">Supprimer</string>
<string name="description_hint">Question (As-tu ... aujourd\'hui?)</string>
<string name="description_hint">Question (As-tu ... aujourd\'hui ?)</string>
<string name="repeat">Répéter</string>
<string name="times_every">fois en</string>
<string name="days">jours</string>
@@ -51,6 +51,7 @@
<string name="streaks">Séries</string>
<string name="no_habits_found">Vous n\'avez pas d\'habitudes actives</string>
<string name="long_press_to_toggle">Appuyez longtemps pour cocher ou décocher</string>
<string name="reminder_off">Aucun</string>
<string name="validation_name_should_not_be_blank">Le nom ne peut être vide.</string>
<string name="validation_number_should_be_positive">Le nombre doit être positif.</string>
<string name="validation_at_most_one_rep_per_day">Vous pouvez avoir au plus une répétition par jour</string>
@@ -67,29 +68,33 @@
<string name="intro_description_3">Les habitudes régulières pendant une période de temps étendue gagneront une étoile complète.</string>
<string name="intro_title_4">Suivez votre progrès</string>
<string name="intro_description_4">Des graphiques détaillés vous montrent comment vos habitudes évoluent au fil du temps.</string>
<string name="interval_15_minutes">15 minutes</string>
<string name="interval_30_minutes">30 minutes</string>
<string name="interval_1_hour">1 heure</string>
<string name="interval_2_hour">2 heures</string>
<string name="interval_4_hour">4 heures</string>
<string name="interval_8_hour">8 heures</string>
<string name="pref_toggle_title">Activer les répétitions avec un appui court</string>
<string name="pref_toggle_description">Plus pratique, mais peut causer des activations accidentelles.</string>
<string name="interval_24_hour">24 heures</string>
<string name="pref_toggle_title">Valider l\'habitude avec un appui court</string>
<string name="pref_toggle_description">Valide l\'habitude avec un appui court plutôt qu\'un appuie long. Plus pratique, mais peut causer des activations accidentelles.</string>
<string name="pref_snooze_interval_title">Intervalle de report des rappels</string>
<string name="pref_rate_this_app">Notez cette app sur le Google Play Store</string>
<string name="pref_send_feedback">Envoyez un avis au développeur</string>
<string name="pref_view_source_code">Voir le code source sur GitHub</string>
<string name="pref_view_app_introduction">Voir l\'intro de l\'app</string>
<string name="pref_view_app_introduction">Voir l\'introduction de l\'app</string>
<string name="links">Liens</string>
<string name="behavior">Comportement</string>
<string name="name">Nom</string>
<string name="settings">Paramètres</string>
<string name="snooze_interval">Intervalle de report</string>
<string name="hint_title">Le saviez-vous ?</string>
<string name="hint_drag">Pour réarranger les habitudes, faites un appui long sur le nom de l\'habitude et placez la à la bonne place.</string>
<string name="hint_drag">Pour réordonner les habitudes, faites un appui long sur le nom de l\'habitude et placez-la à la bonne place.</string>
<string name="hint_landscape">Vous pouvez voir plus de jours en mettant votre téléphone en mode paysage.</string>
<string name="delete_habits">Supprimer des habitudes</string>
<string name="delete_habits_message">Les habitudes seront supprimées définitivement. Cette action ne peut être annulée.</string>
<string name="weekends">Fin de semaine</string>
<string name="any_weekday">Jours de la semaine</string>
<string name="delete_habits_message">Les habitudes seront supprimées définitivement. Cette action est irréversible.</string>
<string name="habit_not_found">Habitude supprimée / introuvable</string>
<string name="weekends">Weekends</string>
<string name="any_weekday">Du lundi au vendredi</string>
<string name="any_day">N\'importe quel jour</string>
<string name="select_weekdays">Sélectionner des jours</string>
<string name="export_to_csv">Exporter les données dans un fichier CSV</string>
@@ -100,8 +105,9 @@
<string name="about">À propos</string>
<string name="translators">Traducteurs</string>
<string name="developers">Développeurs</string>
<string name="version_n">Version %s</string>
<string name="frequency">Fréquence</string>
<string name="checkmark">Croix</string>
<string name="checkmark">Case à cocher</string>
<string name="strength">Force</string>
<string name="best_streaks">Meilleures séries</string>
<string name="current_streaks">Série actuelle</string>
@@ -125,15 +131,16 @@
<string name="import_data">Importer des données</string>
<string name="export_full_backup">Exporter une sauvegarde complète</string>
<string name="import_data_summary">Supporte les sauvegardes complètes générées par cette application, ainsi que les fichiers Tickmate, HabitBull et Rewire. Voir la FAQ pour plus d\'informations.</string>
<string name="export_as_csv_summary">Génère des fichiers pouvant être ouverts par des tableurs comme Microsoft Excel ou LibreOffice Calc. Ces fichiers ne peuvent être réimportés.</string>
<string name="export_as_csv_summary">Génère des fichiers pouvant être ouverts par des tableurs comme Microsoft Excel ou LibreOffice Calc. Ce fichier ne peut pas être réimporté.</string>
<string name="export_full_backup_summary">Génère un fichier contenant toutes vos données. Ce fichier peut être réimporté.</string>
<string name="bug_report_failed">La génération du rapport de bug a échouée.</string>
<string name="generate_bug_report">Générer un rapport de bug.</string>
<string name="troubleshooting">Résolution de problèmes</string>
<string name="help_translate">Aider à traduire cette application</string>
<string name="night_mode">Mode Nuit</string>
<string name="use_pure_black">Utiliser un noir pure dans le mode nuit.</string>
<string name="pure_black_description">Remplacer le fond gris par un noir pure dans le mode nuit; ça réduit lusage de la batterie d\'un appareil ayant un écran AMOLED.</string>
<string name="use_pure_black">Utiliser un noir pur dans le mode nuit</string>
<string name="pure_black_description">Remplacer le fond gris par un noir pur dans le mode nuit ; ça réduit lusage de la batterie des appareils ayant un écran AMOLED.</string>
<string name="interface_preferences">Interface</string>
<string name="reverse_days">Inverser l\'ordre des jours</string>
<string name="reverse_days_description">Montrer les jours dans l\'ordre inversé sur l\'écran principal</string>
<string name="day">Jour</string>
@@ -141,12 +148,31 @@
<string name="month">Mois</string>
<string name="quarter">Trimestre</string>
<string name="year">Année</string>
<string name="total">Total</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">fois tous les</string>
<string name="every_x_days">Tous les %d jours</string>
<string name="every_x_weeks">Toutes les %d semaines</string>
<string name="every_x_months">Tous les %d mois</string>
<string name="score">Pointage</string>
<string name="score">Score</string>
<string name="reminder_sound">Son de rappel</string>
<string name="none">Aucun</string>
<string name="filter">Filtre</string>
<string name="hide_completed">Cacher les habitudes complétées</string>
<string name="hide_archived">Cacher les habitudes archivées</string>
<string name="sticky_notifications">Rendre les notifications persistantes</string>
<string name="sticky_notifications_description">Évite que les notifications ne soient enlevées.</string>
<string name="repair_database">Réparer la base de données</string>
<string name="database_repaired">Base de données réparée.</string>
<string name="uncheck">Décocher</string>
<string name="toggle">Basculer</string>
<string name="action">Action</string>
<string name="habit">Habitude</string>
<string name="sort">Trier</string>
<string name="manually">Manuellement</string>
<string name="by_name">Par nom</string>
<string name="by_color">Par couleur</string>
<string name="by_score">Par score</string>
<string name="download">Télécharger</string>
<string name="export">Exporter</string>
</resources>

View File

@@ -91,6 +91,7 @@
<string name="interval_2_hour">2 घंटा</string>
<string name="interval_4_hour">4 घंटा</string>
<string name="interval_8_hour">8 घंटा</string>
<string name="interval_24_hour">24 घंटे</string>
<string name="pref_toggle_title">टॉगल पुनरावृत्ति हल्का दबाने से</string>
<string name="pref_toggle_description">\"
अधिक सुविधाजनक है, लेकिन आकस्मिक टॉगल हो सकता है ।\"</string>
@@ -179,7 +180,7 @@ repetitions की संख्या\"</string>
इस फ़ाइल में वापस आयात नहीं किया जा सकता है।\"</string>
<string name="export_full_backup_summary">ऐसी फाइल्स उत्पन्न करता है जिसमे आपका सारा डेटा रहता है इस फ़ाइल को वापस आयात किया जा सकता है।</string>
<string name="bug_report_failed">बग रिपोर्ट जनरेट करने मे असफल</string>
<string name="generate_bug_report">बग रिपोर्ट जनरेट करने मे सफल</string>
<string name="generate_bug_report">बग रिपोर्ट जनरेट करें</string>
<string name="troubleshooting">\"
समस्या निवारण\"</string>
<string name="help_translate">\"
@@ -198,15 +199,26 @@ repetitions की संख्या\"</string>
<string name="quarter">तिमाही</string>
<string name="year">साल</string>
<!-- Middle part of the sentence '1 time in xx days' -->
<string name="time_every">समय शुरू</string>
<string name="time_every">समय में</string>
<string name="every_x_days">\"
हर %d दिन\"</string>
<string name="every_x_weeks">\"
हर %d हफ्ते\"</string>
<string name="every_x_months">\"
हर %d साल\"</string>
हर %d महीने\"</string>
<string name="score">स्कोर</string>
<string name="reminder_sound">अनुस्मारक ध्वनि</string>
<string name="none">\"
कोई आवाज नहीं\"</string>
<string name="filter">फिल्टर</string>
<string name="repair_database">डेटाबेस को रिपेयर करें</string>
<string name="database_repaired">डेटाबेस रिपेयर सफल</string>
<string name="habit">आदत</string>
<string name="sort">सॉर्ट करें</string>
<string name="manually">मैन्यूअली</string>
<string name="by_name">नाम द्वारा</string>
<string name="by_color">रंग द्वारा</string>
<string name="by_score">स्कोर से</string>
<string name="download">डाउनलोड</string>
<string name="export">एक्सपोर्ट करे</string>
</resources>

View File

@@ -78,9 +78,9 @@
<string name="pref_toggle_title">Označi ponavljanja sa kratkim pritisk.</string>
<string name="pref_toggle_description">Zgodnije je, no može uzrokovati slučajna označavanja.</string>
<string name="pref_snooze_interval_title">Interval odgađanja na podsjetnicima</string>
<string name="pref_rate_this_app">Ocijenite ovu aplikaciju na Google Playu</string>
<string name="pref_send_feedback">Pošaljite povratne informacije raz. programeru</string>
<string name="pref_view_source_code">Pogledajte izvorni kod na GitHubu</string>
<string name="pref_rate_this_app">Ocijeni ovu aplikaciju na Google Playu</string>
<string name="pref_send_feedback">Pošalji povratne informacije raz. programeru</string>
<string name="pref_view_source_code">Pogledaj izvorni kod na GitHubu</string>
<string name="pref_view_app_introduction">Prikaži uvod u aplikaciju</string>
<string name="links">Poveznice</string>
<string name="behavior">Ponašanje</string>
@@ -136,13 +136,13 @@
<string name="bug_report_failed">Generiranje izvješća o pogrešci nije uspjelo.</string>
<string name="generate_bug_report">Generiraj izvješće o pogreški</string>
<string name="troubleshooting">Rješavanje problema</string>
<string name="help_translate">Pomozite prevesti ovu aplikaciju</string>
<string name="help_translate">Pomozi prevesti ovu aplikaciju</string>
<string name="night_mode">Noćni način</string>
<string name="use_pure_black">Koristi crnu boju za noćni način</string>
<string name="pure_black_description">Zamjenjuje sivu pozadinu sa crnom u noćnom načinu. To smanjuje potrošnju bateriju na uređajima s AMOLED zaslonima.</string>
<string name="interface_preferences">Sučelje</string>
<string name="reverse_days">Obrnuti poredak dana</string>
<string name="reverse_days_description">Prikaži dane obrnutim redom na glavnom zaslonu</string>
<string name="reverse_days_description">Prikažite dane obrnutim redom na glavnom zaslonu</string>
<string name="day">Dan</string>
<string name="week">Tjedan</string>
<string name="month">Mjesec</string>
@@ -158,7 +158,7 @@
<string name="reminder_sound">Zvuk podsjetnika</string>
<string name="none">Nijedan</string>
<string name="filter">Filtar</string>
<string name="hide_completed">Skrivanje je uspjelo</string>
<string name="hide_completed">Sakrij završeno</string>
<string name="hide_archived">Sakrij arhivirano</string>
<string name="sticky_notifications">Učini obavijesti trajnima</string>
<string name="sticky_notifications_description">Spriječava da se obavijesti zanemare.</string>

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