diff --git a/CHANGELOG.md b/CHANGELOG.md index 16887c998..5c7278a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 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 diff --git a/app/build.gradle b/app/build.gradle index 4d823549b..3ad44b358 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,8 +5,8 @@ apply plugin: 'jacoco' apply plugin: 'com.github.triplet.play' android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion 27 + buildToolsVersion "27.0.3" // signingConfigs { // release { @@ -27,7 +27,7 @@ android { defaultConfig { applicationId "org.isoron.uhabits" minSdkVersion 15 - targetSdkVersion 25 + targetSdkVersion 27 buildConfigField "Integer", "databaseVersion", "15" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" @@ -73,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' @@ -84,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' diff --git a/app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java b/app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java deleted file mode 100644 index 631f2d396..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -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); - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 10f380b86..9bba07137 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,8 +21,8 @@ + android:versionCode="36" + android:versionName="1.7.9"> diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index ca97cf609..aa3d8dd52 100644 Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/com/activeandroid/ActiveAndroid.java b/app/src/main/java/com/activeandroid/ActiveAndroid.java new file mode 100644 index 000000000..c58c8efd8 --- /dev/null +++ b/app/src/main/java/com/activeandroid/ActiveAndroid.java @@ -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); + } +} diff --git a/app/src/main/java/com/activeandroid/Cache.java b/app/src/main/java/com/activeandroid/Cache.java new file mode 100644 index 000000000..6495e3790 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Cache.java @@ -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 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(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 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 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 getTableInfos() { + return sModelInfo.getTableInfos(); + } + + public static synchronized TableInfo getTableInfo(Class type) { + return sModelInfo.getTableInfo(type); + } + + public static synchronized TypeSerializer getParserForType(Class type) { + return sModelInfo.getTypeSerializer(type); + } + + public static synchronized String getTableName(Class type) { + return sModelInfo.getTableInfo(type).getTableName(); + } +} diff --git a/app/src/main/java/com/activeandroid/Configuration.java b/app/src/main/java/com/activeandroid/Configuration.java new file mode 100644 index 000000000..b197d2234 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Configuration.java @@ -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> mModelClasses; + private List> 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> getModelClasses() { + return mModelClasses; + } + + public List> 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> mModelClasses; + private List> 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 modelClass) { + if (mModelClasses == null) { + mModelClasses = new ArrayList>(); + } + + mModelClasses.add(modelClass); + return this; + } + + public Builder addModelClasses(Class... modelClasses) { + if (mModelClasses == null) { + mModelClasses = new ArrayList>(); + } + + mModelClasses.addAll(Arrays.asList(modelClasses)); + return this; + } + + public Builder setModelClasses(Class... modelClasses) { + mModelClasses = Arrays.asList(modelClasses); + return this; + } + + public Builder addTypeSerializer(Class typeSerializer) { + if (mTypeSerializers == null) { + mTypeSerializers = new ArrayList>(); + } + + mTypeSerializers.add(typeSerializer); + return this; + } + + public Builder addTypeSerializers(Class... typeSerializers) { + if (mTypeSerializers == null) { + mTypeSerializers = new ArrayList>(); + } + + mTypeSerializers.addAll(Arrays.asList(typeSerializers)); + return this; + } + + public Builder setTypeSerializers(Class... 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> loadModelList(String[] models) { + final List> modelClasses = new ArrayList>(); + 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> loadSerializerList(String[] serializers) { + final List> typeSerializers = new ArrayList>(); + 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; + } + + } +} diff --git a/app/src/main/java/com/activeandroid/DatabaseHelper.java b/app/src/main/java/com/activeandroid/DatabaseHelper.java new file mode 100644 index 000000000..7158c5bb6 --- /dev/null +++ b/app/src/main/java/com/activeandroid/DatabaseHelper.java @@ -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 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 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); + + } + } +} diff --git a/app/src/main/java/com/activeandroid/Model.java b/app/src/main/java/com/activeandroid/Model.java new file mode 100644 index 000000000..4d22d9c89 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Model.java @@ -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 type, long id) { + TableInfo tableInfo = Cache.getTableInfo(type); + new Delete().from(type).where(tableInfo.getIdName()+"=?", id).execute(); + } + + public static T load(Class 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 columnsOrdered = new ArrayList(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 entityType = (Class) 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 enumType = (Class) 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 List getMany(Class 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. + } +} diff --git a/app/src/main/java/com/activeandroid/ModelInfo.java b/app/src/main/java/com/activeandroid/ModelInfo.java new file mode 100644 index 000000000..09e79117c --- /dev/null +++ b/app/src/main/java/com/activeandroid/ModelInfo.java @@ -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, TableInfo> mTableInfos = new HashMap, TableInfo>(); + private Map, TypeSerializer> mTypeSerializers = new HashMap, 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 getTableInfos() { + return mTableInfos.values(); + } + + public TableInfo getTableInfo(Class 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> models = configuration.getModelClasses(); + if (models != null) { + for (Class model : models) { + mTableInfos.put(model, new TableInfo(model)); + } + } + + final List> typeSerializers = configuration.getTypeSerializers(); + if (typeSerializers != null) { + for (Class 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 paths = new ArrayList(); + + if (sourcePath != null && !(new File(sourcePath).isDirectory())) { + DexFile dexfile = new DexFile(sourcePath); + Enumeration entries = dexfile.entries(); + + while (entries.hasMoreElements()) { + paths.add(entries.nextElement()); + } + } + // Robolectric fallback + else { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration 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 modelClass = (Class) 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); + } + } + } +} diff --git a/app/src/main/java/com/activeandroid/TableInfo.java b/app/src/main/java/com/activeandroid/TableInfo.java new file mode 100644 index 000000000..32d1ecb3f --- /dev/null +++ b/app/src/main/java/com/activeandroid/TableInfo.java @@ -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 mType; + private String mTableName; + private String mIdName = Table.DEFAULT_ID_NAME; + + private Map mColumnNames = new LinkedHashMap(); + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + public TableInfo(Class 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 fields = new LinkedList(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 getType() { + return mType; + } + + public String getTableName() { + return mTableName; + } + + public String getIdName() { + return mIdName; + } + + public Collection 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; + } + +} diff --git a/app/src/main/java/com/activeandroid/annotation/Column.java b/app/src/main/java/com/activeandroid/annotation/Column.java new file mode 100644 index 000000000..56bbcde85 --- /dev/null +++ b/app/src/main/java/com/activeandroid/annotation/Column.java @@ -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 {}; +} diff --git a/app/src/main/java/com/activeandroid/annotation/Table.java b/app/src/main/java/com/activeandroid/annotation/Table.java new file mode 100644 index 000000000..541dfbe92 --- /dev/null +++ b/app/src/main/java/com/activeandroid/annotation/Table.java @@ -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; +} diff --git a/app/src/main/java/com/activeandroid/query/Delete.java b/app/src/main/java/com/activeandroid/query/Delete.java new file mode 100644 index 000000000..6d19dcedc --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Delete.java @@ -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 table) { + return new From(table, this); + } + + @Override + public String toSql() { + return "DELETE "; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/From.java b/app/src/main/java/com/activeandroid/query/From.java new file mode 100644 index 000000000..825e36db7 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/From.java @@ -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 mType; + private String mAlias; + private List mJoins; + private final StringBuilder mWhere = new StringBuilder(); + private String mGroupBy; + private String mHaving; + private String mOrderBy; + private String mLimit; + private String mOffset; + + private List mArguments; + + public From(Class table, Sqlable queryBase) { + mType = table; + mJoins = new ArrayList(); + mQueryBase = queryBase; + + mJoins = new ArrayList(); + mArguments = new ArrayList(); + } + + public From as(String alias) { + mAlias = alias; + return this; + } + + public Join join(Class table) { + Join join = new Join(this, table, null); + mJoins.add(join); + return join; + } + + public Join leftJoin(Class table) { + Join join = new Join(this, table, JoinType.LEFT); + mJoins.add(join); + return join; + } + + public Join outerJoin(Class table) { + Join join = new Join(this, table, JoinType.OUTER); + mJoins.add(join); + return join; + } + + public Join innerJoin(Class table) { + Join join = new Join(this, table, JoinType.INNER); + mJoins.add(join); + return join; + } + + public Join crossJoin(Class 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 List execute() { + if (mQueryBase instanceof Select) { + return SQLiteUtils.rawQuery(mType, toSql(), getArguments()); + + } else { + SQLiteUtils.execSql(toSql(), getArguments()); + return null; + + } + } + + public 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 true if the query returns at least one row; otherwise, false. + */ + 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; + } +} diff --git a/app/src/main/java/com/activeandroid/query/Join.java b/app/src/main/java/com/activeandroid/query/Join.java new file mode 100644 index 000000000..13cdba3be --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Join.java @@ -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 mType; + private String mAlias; + private JoinType mJoinType; + private String mOn; + private String[] mUsing; + + Join(From from, Class 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(); + } +} diff --git a/app/src/main/java/com/activeandroid/query/Select.java b/app/src/main/java/com/activeandroid/query/Select.java new file mode 100644 index 000000000..1d4c64885 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Select.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/Set.java b/app/src/main/java/com/activeandroid/query/Set.java new file mode 100644 index 000000000..183d99f0f --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Set.java @@ -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 mSetArguments; + private List mWhereArguments; + + public Set(Update queryBase, String set) { + mUpdate = queryBase; + mSet = set; + + mSetArguments = new ArrayList(); + mWhereArguments = new ArrayList(); + } + + public Set(Update queryBase, String set, Object... args) { + mUpdate = queryBase; + mSet = set; + + mSetArguments = new ArrayList(); + mWhereArguments = new ArrayList(); + + 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; + } +} diff --git a/app/src/main/java/com/activeandroid/query/Sqlable.java b/app/src/main/java/com/activeandroid/query/Sqlable.java new file mode 100644 index 000000000..2c3f5d437 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Sqlable.java @@ -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(); +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/Update.java b/app/src/main/java/com/activeandroid/query/Update.java new file mode 100644 index 000000000..a69d2d8e0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Update.java @@ -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 mType; + + public Update(Class 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 getType() { + return mType; + } + + @Override + public String toSql() { + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE "); + sql.append(Cache.getTableName(mType)); + sql.append(" "); + + return sql.toString(); + } +} diff --git a/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java b/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java new file mode 100644 index 000000000..333f900f0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java b/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java new file mode 100644 index 000000000..55509bd08 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/FileSerializer.java b/app/src/main/java/com/activeandroid/serializer/FileSerializer.java new file mode 100644 index 000000000..0aed072c0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/FileSerializer.java @@ -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); + } +} diff --git a/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java b/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java new file mode 100644 index 000000000..530d1249b --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java b/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java new file mode 100644 index 000000000..af0a21ded --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java @@ -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); +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java b/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java new file mode 100644 index 000000000..94ba37ff3 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java b/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java new file mode 100644 index 000000000..a82c7ef15 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/IOUtils.java b/app/src/main/java/com/activeandroid/util/IOUtils.java new file mode 100644 index 000000000..b3005f857 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/IOUtils.java @@ -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 { + + /** + *

+ * Unconditionally close a {@link Closeable}. + *

+ * 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); + } + } + + /** + *

+ * Unconditionally close a {@link Cursor}. + *

+ * 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); + } + } +} diff --git a/app/src/main/java/com/activeandroid/util/Log.java b/app/src/main/java/com/activeandroid/util/Log.java new file mode 100644 index 000000000..1c2a384d5 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/Log.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java b/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java new file mode 100644 index 000000000..b09de26fa --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java @@ -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 + + Based on the C version by Martin Pool, of which this is more or less a straight conversion. + Copyright (C) 2000 by Martin Pool + + 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 { + 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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/ReflectionUtils.java b/app/src/main/java/com/activeandroid/util/ReflectionUtils.java new file mode 100644 index 000000000..32e995cc3 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/ReflectionUtils.java @@ -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 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 getDeclaredColumnFields(Class type) { + Set declaredColumnFields = Collections.emptySet(); + + if (ReflectionUtils.isSubclassOf(type, Model.class) || Model.class.equals(type)) { + declaredColumnFields = new LinkedHashSet(); + + Field[] fields = type.getDeclaredFields(); + Arrays.sort(fields, new Comparator() { + @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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/SQLiteUtils.java b/app/src/main/java/com/activeandroid/util/SQLiteUtils.java new file mode 100644 index 000000000..cbf41eaee --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/SQLiteUtils.java @@ -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, SQLiteType> TYPE_MAP = new HashMap, 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> sIndexGroupMap; + private static HashMap> sUniqueGroupMap; + private static HashMap 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 List rawQuery(Class type, String sql, String[] selectionArgs) { + Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs); + List 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 rawQuerySingle(Class type, String sql, String[] selectionArgs) { + List entities = rawQuery(type, sql, selectionArgs); + + if (entities.size() > 0) { + return entities.get(0); + } + + return null; + } + + // Database creation + + public static ArrayList createUniqueDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + sUniqueGroupMap = new HashMap>(); + sOnUniqueConflictsMap = new HashMap(); + + for (Field field : tableInfo.getFields()) { + createUniqueColumnDefinition(tableInfo, field); + } + + if (sUniqueGroupMap.isEmpty()) { + return definitions; + } + + Set keySet = sUniqueGroupMap.keySet(); + for (String key : keySet) { + List 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 list = sUniqueGroupMap.get(group); + if (list == null) { + list = new ArrayList(); + } + list.add(name); + + sUniqueGroupMap.put(group, list); + sOnUniqueConflictsMap.put(group, conflictAction); + } + } + + public static String[] createIndexDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + sIndexGroupMap = new HashMap>(); + + for (Field field : tableInfo.getFields()) { + createIndexColumnDefinition(tableInfo, field); + } + + if (sIndexGroupMap.isEmpty()) { + return new String[0]; + } + + for (Map.Entry> 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 list = new ArrayList(); + list.add(name); + sIndexGroupMap.put(name, list); + } + + String[] groups = column.indexGroups(); + for (String group : groups) { + if (TextUtils.isEmpty(group)) + continue; + + List list = sIndexGroupMap.get(group); + if (list == null) { + list = new ArrayList(); + } + + list.add(name); + sIndexGroupMap.put(group, list); + } + } + + public static String createTableDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + + 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) 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 List processCursor(Class type, Cursor cursor) { + TableInfo tableInfo = Cache.getTableInfo(type); + String idName = tableInfo.getIdName(); + final List entities = new ArrayList(); + + 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 columnsOrdered = new ArrayList(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 lexSqlScript(String sqlScript) { + ArrayList sl = new ArrayList(); + 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; + } +} diff --git a/app/src/main/java/com/activeandroid/util/SqlParser.java b/app/src/main/java/com/activeandroid/util/SqlParser.java new file mode 100644 index 000000000..f9531b7c8 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/SqlParser.java @@ -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 parse(final InputStream stream) throws IOException { + + final BufferedInputStream buffer = new BufferedInputStream(stream); + final List commands = new ArrayList(); + 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 == ' '; + } +} diff --git a/app/src/main/java/com/activeandroid/util/Tokenizer.java b/app/src/main/java/com/activeandroid/util/Tokenizer.java new file mode 100644 index 000000000..8ae34da32 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/Tokenizer.java @@ -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; + } +} diff --git a/app/src/main/java/com/activeandroid/widget/ModelAdapter.java b/app/src/main/java/com/activeandroid/widget/ModelAdapter.java new file mode 100644 index 000000000..a38957636 --- /dev/null +++ b/app/src/main/java/com/activeandroid/widget/ModelAdapter.java @@ -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 extends ArrayAdapter { + 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 objects) { + super(context, textViewResourceId, objects); + } + + public ModelAdapter(Context context, int resource, int textViewResourceId, List objects) { + super(context, resource, textViewResourceId, objects); + } + + /** + * Clears the adapter and, if data != null, fills if with new Items. + * + * @param collection A Collection<? extends T> which members get added to the adapter. + */ + public void setData(Collection 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; + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java index 330e05b34..ee540597c 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java @@ -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); } diff --git a/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java b/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java index d6a0253a2..15fccba6a 100644 --- a/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java +++ b/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java @@ -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,38 @@ 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); + notificationManager.notify(notificationId, builder.build()); } private boolean shouldShowReminderToday() @@ -245,4 +253,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); + } + } } diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml index 1768b2c4c..86bafd4f7 100644 --- a/app/src/main/res/layout/about.xml +++ b/app/src/main/res/layout/about.xml @@ -186,6 +186,10 @@ style="@style/About.Item" android:text="Can Altas (Deutsch)"/> + + @@ -194,6 +198,10 @@ style="@style/About.Item" android:text="Beriain (Euskara)"/> + + @@ -206,6 +214,10 @@ style="@style/About.Item" android:text="Saeed Esmaili (Fārsi)"/> + + @@ -222,6 +234,10 @@ style="@style/About.Item" android:text="Michael Faille (Français)"/> + + @@ -262,6 +278,10 @@ style="@style/About.Item" android:text="Andrei Pleș (Română)"/> + + @@ -274,6 +294,10 @@ style="@style/About.Item" android:text="Robin (Svenska)"/> + + @@ -282,6 +306,10 @@ style="@style/About.Item" android:text="Caner Başaran (Türkçe)"/> + + @@ -294,6 +322,10 @@ style="@style/About.Item" android:text="Oglaigh Rystard (Українська)"/> + + @@ -326,6 +358,14 @@ style="@style/About.Item" android:text="Al Alloush (العَرَبِية‎)"/> + + + + @@ -350,6 +390,30 @@ style="@style/About.Item" android:text="Mahdi Nasiri (فارسی‎)"/> + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..47f7ad011 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index fe08f96ff..865214f73 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..5b2b4a4f4 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8d5f7de42 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..657fbeb32 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..66e6f7341 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..3d5ef620f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index c4e4bfa59..fb8807ad2 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..49476926d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index d994b3aca..ccbf7c9b8 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -19,6 +19,34 @@ ~ with this program. If not, see . --> + Herhaalde Gewoonte Boekhouer + Gewoontes + Instellings + Redigeer + Verwyder + Argiveer + Deargiveer + Voeg gewoonte by + Verander kleur + Gewoonte geskep + Gewoontes verwyder + Gewoontes herstel + Niks om terug te doen nie + Niks om oor te doen nie + Gewoonte verander + Gewoonte terug verander + Gewoontes geargiveer + Gewoontes gedeargiveer + Oorsig + Welkom + 15 minute + 30 minute + 1 uur + 2 ure + 4 ure + 8 ure + 24 ure + Instellings diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a0dda8c3c..b624961af 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -19,63 +19,63 @@ ~ with this program. If not, see . --> - لوب ملاحق العادة + متعقب العادة لووب عادات إعدادات تعديل حذف - أرشيف + أرشفة إزالة من الأرشيف - إضافة العادة - غير اللون - تم صنع عادة - تم حذف عادة - تم ترجيع عادة - لا شيء للتراجع - لا شيء لتكرار - تم تغييرعادة - تم ترجيع العادة إلى أصلها - تم أرشيف العادات - تم إزالة العادة من الأرشيف + إضافة عادة + تغيير اللون + تم إنشاء عادة + تم حذف العادات + تم إستعادة العادات + لا شيء للألغاء + لا شيء للإعادة + تم تغيير عادة + تم أرجاع العادة إلى أصلها + تم أرشفه العادات + تم الغاء ارشفه العادات نظرة عامة قوة العادة - التاريخ - مسح + السجل + إزالة السؤال (هل ... اليوم؟) - كرر - مرات في + كرره + مرات كل أيام - تذكير - حذف + التذكرة + تجاهل حفظ - تقدم متتالية - لا يوجد لديك عادات مفعله + الانجازات + لا يوجد لديك عادات مفعلة أضغط و إستمر لتحقق أو ازل - أوقف + إيقاف لا يمكن أن يكون الإسم فارغ - يجب أن يكون الرقم إيجابي - يمكن أن يكون التكرار واحدة فقط كل يوم - اخلق عادة + يجب أن يكون الرقم موجب. + يجب أن يكون التكرار مرة واحدة فقط كل يوم + انشاء العادة تعديل العادة حقق - لاحقا + لاحقاً أهلا بك - لوب يساعدك على خلق والحفاظ على العادات الجيدة. - إنشاء بعض عادات جديدة - كل يوم، بعد أداء عادتك، وضع علامة على التطبيق. + لوب يساعدك في بدأ عادات جيدة والحفاظ عليها. + إنشاء عادات جديدة + كل يوم، بعد أداء عادتك، ضع علامة عليها في التطبيق. حافظ على القيام بذلك - العادة المستمرة لفترات طويلة تكسب نجمة كامله - تتبع تقدمك - رسوم بيانية مفصلة تبين لكم كيف تحسن عاداتك مع مرور الوقت. + العادة المستمرة لفترة طويلة تكسب نجمة كامله. + تتبع اداءك + رسوم بيانية مفصلة تُريك كيف تحسنت عاداتك مع مرور الوقت. 15 دقيقة 30 دقيقة ساعة واحدة - ساعتين - أربع ساعات - ثماني ساعات - 24 ساعة - تبديل بكبسه + ساعتان + ٤ ساعات + 8 ساعات + ٢٤ ساعة + تبديل وضعية العادة بضغطة قصيرة أكثر سهولة، لكنه ممكن يسبب كبسات غير مقصوده فترتي الغفوى على التذكير تقييم هذا التطبيق على جوجل بلاي @@ -92,6 +92,7 @@ يمكنك ان ترى المزيد أيام عن طريق وضع الهاتف في وضع أفقي. حذف عادات سيتم حذف عادات بشكل دائم. هذا العمل لا يمكن التراجع عنه. + العادة حذفت/لم يتم العثور عليها عطلة نهاية الأسبوع أيام الأسبوع أي يوم @@ -156,7 +157,22 @@ النقاط صوت تذكير صامت + تصنيف + إخفاء المكتملة + إخفاء المؤرشفة + جعل الإشعارات ثابتة + منع الإشعارات من تمريرها بعيداً. + إصلاح قاعدة البيانات + تم إصلاح قاعدة البيانات. + إلغاء تحديد + تبديل عمل + عادة + فرز + يدوياً + حسب الإسم + حسب اللون + حسب النقاط تحميل استخراج diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 79910b24b..912aa2c41 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -61,7 +61,7 @@ Später Willkommen - Loop Habit Tracker hilft dir gute Gewohnheiten anzueignen. + Loop Habit Tracker hilft dir dabei, gute Gewohnheiten anzunehmen. Erstelle neue Gewohnheiten Hake die Gewohnheit jeden Tag in der App ab, nachdem du sie erledigt hast. Bleib dran @@ -75,7 +75,7 @@ 4 Stunden 8 Stunden 24 Stunden - Markierung durch kurzes Tippen ändern + Markierung durch kurzes Drücken ändern Markierungen durch einfaches Tippen setzen anstatt durch Tippen und Halten. Bequemer, kann aber versehentlich eine Markierung ändern. \"Später erinnern\"-Intervall bei Erinnerungen Bewerte diese App auf Google Play @@ -94,7 +94,7 @@ Die Gewohnheit wird für immer gelöscht. Dies kann nicht rückgängig gemacht werden. Gewohnheit gelöscht / nicht gefunden An Wochenenden - Werktags + Montag bis Freitag Jeden Tag Wähle Tage aus Exportiere als CSV diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index d994b3aca..31c69a0e8 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -19,6 +19,76 @@ ~ with this program. If not, see . --> + Kutimoj + Agordoj + Redakti + Forigi + Arĥivo + Elarĥivigi + Aldonu kutimon + Ŝanĝi koloron + Kutimo ŝanĝita + Kutimo arĥivita + Kutimo forteco + tagoj + Memorigaĵoj + Nuligi + Konservi + Strioj + Neaktiva + Poste + Bonvenon + 15 minutoj + 30 minutoj + Agordoj + Forigi kutimojn + Semajnfinoj + Lundo al vendredo + Io semajntago + Elekti tagojn + Eksporti kiel CSV + Farite + Elekti horojn + Elekti minutojn + Pri programo + Tradukantoj + Evoluigantoj + Versio %s + Frekvenco + Forteco + Nombro de ripetoj + Lastaj %d tagoj + Lastaj %d semajnoj + Lastaj %d monatoj + Lastaj %d jaroj + Ĉiuj tempoj + Ĉiu tago + Ĉiu semajno + Dufoje en semajno + Kvinfoje en semajno + Helpo & Ofte Demandite + Dosiero ne rekonita. + Plena savkopio sukcese eksportita. + Problemserĉado + Nokta reĝimo + Tago + Semajno + Monato + Jarkvarono + Jaro + Nenio + Filtrilo + Kaŝi kompletajn + Kaŝi arĥivitajn + Ripari datumbazon + Datumbazon riparita. + Ago + Kutimo + Enkursigi + Laŭ nomo + Laŭ koloro + Elŝuti + Eksporti diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2131e3d22..ae68edf48 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -28,20 +28,20 @@ Desarchivar Agregar hábito Cambiar color - Hábito creado. - Hábitos eliminados. - Hábitos restaurados. - Nada que deshacer. - Nada que rehacer. - Hábito cambiado. - Hábito cambiado nuevamente. - Hábitos archivados. - Hábitos desarchivados. + Hábito creado + Hábitos eliminados + Hábitos restaurados + Nada que deshacer + Nada que rehacer + Hábito cambiado + Cambio en hábito vuelto atrás + Hábitos archivados + Hábitos desarchivados Resumen Fuerza del hábito Historial - Eliminar - Pregunta (Has ___ hoy?) + Borrar + Pregunta (Has ... hoy?) Repetir veces cada días @@ -64,7 +64,7 @@ Loop Analizador de Hábitos te ayuda a crear y mantener buenos hábitos. Crea algunos hábitos nuevos Cada día, después de realizar tu hábito, pon una marca en la aplicación. - Sigue haciéndolo. + Sigue haciéndolo Los hábitos realizados consistentemente por un largo tiempo ganarán una estrella completa. Haz un seguimiento de tu progreso Gráficos detallados muestran cómo mejoraron sus hábitos con el tiempo. @@ -75,9 +75,9 @@ 4 horas 8 horas 24 horas - Marca las repeticiones con una pulsación corta. + Marca las repeticiones con una pulsación corta Más cómodo, pero puede causar marcas accidentales. - Tiempo de espera al aplazar recordatorios. + Tiempo de espera al aplazar recordatorios Valora esta aplicación en Google Play Enviar sugerencias al desarrollador Ver código fuente en GitHub @@ -88,20 +88,20 @@ Configuración Intervalo de espera ¿Sabías qué? - Para reordenar las entradas, mantén la pulsación sobre el nombre del hábito, después arrástralo a su posición correcta. + Para reordenar las entradas, mantén la pulsado sobre el nombre del hábito, después arrástralo a su posición correcta. Puedes ver más días al poner tu teléfono en modo horizontal. Eliminar Hábitos Los hábitos serán eliminados permanentemente. Esta acción no se puede deshacer. Hábito eliminado / no encontrado Fines de semana - Días laborables + De lunes a viernes Cada día Seleccionar días Exportar datos (CSV) Hecho Quitar Seleccionar horas - Seleccionar + Seleccionar minutos Acerca de Traductores Desarrolladores diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 75e38eb2c..d9a4e0a74 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -19,24 +19,24 @@ ~ with this program. If not, see . --> - Ohitura Tracker Loop + Loop Habit Tracker Ohiturak Ezarpenak Editatu Ezabatu Artxibatu - Ezartxibatu - Ohitura gehitu + Desartxibatu + Gehitu ohitura Kolorea aldatu - Ohitura sortu da. - Ohiturak ezabatu dira. - Ohiturak berrezarri dira. - Ez dago desegiteko ezer. - Ez dago berregiteko ezer. - Ohitura aldatu da. - Ohitura lehengoratu da. - Ohiturak artxibatu dira. - Ohiturak ezartxibatu dira. + Ohitura sortu da + Ohiturak ezabatu dira + Ohiturak berrezarri dira + Ez dago ezer desegiteko + Ez dago ezer berregiteko + Ohitura aldatu egin da + Ohitura berrezarri da + Ohiturak artxibatu dira + Ohiturak desartxibatu dira Ikuspegi orokorra Ohituraren indarra Historia @@ -58,10 +58,10 @@ Ohitura sortu Ohitura editatu Markatu - Beranduago + Geroago Ongi etorri - Loop Habit Trackerek ohitura onak hartzen eta mantentzen laguntzen dizu. + Loop Habit Tracker-ek ohitura onak hartzen eta mantentzen laguntzen dizu. Sor itzazu ohitura berri batzuk Egunero, zure ohitura egin ostean, jarri ezazu egiaztatze marka bat aplikazioan. Jarrai ezazu ohitura egiten @@ -77,7 +77,7 @@ 24 ordu Ukitze laburrarekin markatu Ukitze bakar batekin marka jartzen du ukitu eta mantendu egin beharrean. Erosoagoa, baina nahi gabeko markak ekar litzake. - Atzeratze tartea gogorarazpenetan + Atzeratze tartea oroigarrietan Aplikazio hau Google Playen puntuatu Zure iritzia garatzaileari bidali Iturburu kodea GitHuben ikusi @@ -158,7 +158,7 @@ Oroigarriaren soinua Bat ere ez Iragazkia - Lortutakoak ezkutatu + Ezkutatu lortutakoak Artxibatutakoak ezkutatu Jakinarazpenak itsaskorrak bihurtu Jakinarazpenak keinu batez ezabatzea sahiesten du. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index c3512ccde..82a96ccd2 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -19,24 +19,24 @@ ~ with this program. If not, see . --> - عادت‌سنج لوپ + Loop Habit Tracker عادت‌ها تنظیمات ویرایش - حذف کن + حذف بایگانی کن خارج کردن از بایگانی افزودن عادت تغییر رنگ - عادت ساخته شد. - عادت حذف شد. - عادت بازگردانده شد. + عادت ایجاد شد + عادت حذف شد + عادت بازگردانده شد چیزی برای بازگرداندن به حالت قبلی وجود ندارد چیزی برای انجام مجدد وجود ندارد عادت تغییر کرد. عادت به حالت قبل برگشت - عادات بایگانی شدند - عادت از بایگانی خارج شدند + عادت‌ها بایگانی شدند + عادت‌ها از بایگانی خارج شدند مرور قدرت عادت تاریخچه diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index d994b3aca..f3d86dc67 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -19,6 +19,65 @@ ~ with this program. If not, see . --> + Rutiini - Tracker + Rutiinit + Asetukset + Muokkaa + Poista + Arkistoi + Lisää rutiini + Vaihda väriä + Rutiini luotu + Rutiinit poistettu + Rutiinit palautettu + Rutiini muutettu + Rutiini muutettu takaisin + Yleiskatsaus + Historia + Tyhjennä + Kysymys (Teitkö... tänään?) + Toista + kertaa + päivässä + Muistutus + Hylkää + Tallenna + Pisimmät toistot + Ei aktiivisia rutiineja + Paina pitkään merkitäksesi suoritetuksi tai postaaksesi suorituksen + Pois päältä + Nimi ei voi olla tyhjä. + Luvun on oltava positiivinen. + Luo rutiini + Muokkaa rutiinia + Tehty + Lykkää + Tervetuloa + Merkitse uusia rutiineja + Joka päivä, suoritettuasi rutiinin, merkitse se sovellukseen. + Linkit + Käyttäytyminen + Nimi + Asetukset + Tiesitkö? + Valmis + Tyhjennä + Kääntäjät + Kehittäjät + Versio %s + Joka päivä + Joka viikko + 2 kertaa viikossa + 5 kertaa viikossa + Mukautettu… + Yötila + Käytä puhdasta mustaa yötilassa + Päivä + Viikko + Kuukausi + Kvartaali + Vuosi + Yhteensä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3892fc1ee..eab302fe4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -29,8 +29,8 @@ Ajouter une habitude Changer la couleur Habitude créée - Habitude supprimée - Habitude rétablie + Habitudes supprimées + Habitudes rétablies Rien à annuler Rien à refaire Habitude changée @@ -75,8 +75,8 @@ 4 heures 8 heures 24 heures - Activer les répétitions avec un appui court - Pointe l\'habitude avec un appui court plutôt qu\'un appuie long. Plus pratique, mais peut causer des activations accidentelles. + Valider l\'habitude avec un appui court + Valide l\'habitude avec un appui court plutôt qu\'un appuie long. Plus pratique, mais peut causer des activations accidentelles. Intervalle de report des rappels Notez cette app sur le Google Play Store Envoyez un avis au développeur @@ -91,10 +91,10 @@ Pour réordonner les habitudes, faites un appui long sur le nom de l\'habitude et placez-la à la bonne place. Vous pouvez voir plus de jours en mettant votre téléphone en mode paysage. Supprimer des habitudes - Les habitudes seront supprimées définitivement. Cette action ne peut être annulée. + Les habitudes seront supprimées définitivement. Cette action est irréversible. Habitude supprimée / introuvable - Fin de semaine - Jours de la semaine + Weekends + Du lundi au vendredi N\'importe quel jour Sélectionner des jours Exporter les données dans un fichier CSV @@ -107,7 +107,7 @@ Développeurs Version %s Fréquence - Croix + Case à cocher Force Meilleures séries Série actuelle @@ -131,7 +131,7 @@ Importer des données Exporter une sauvegarde complète 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. - 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. + 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é. Génère un fichier contenant toutes vos données. Ce fichier peut être réimporté. La génération du rapport de bug a échouée. Générer un rapport de bug. @@ -154,7 +154,7 @@ Tous les %d jours Toutes les %d semaines Tous les %d mois - Pointage + Score Son de rappel Aucun Filtre @@ -162,7 +162,7 @@ Cacher les habitudes archivées Rendre les notifications persistantes Évite que les notifications ne soient enlevées. - Réparer le base de données + Réparer la base de données Base de données réparée. Décocher Basculer diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0b1435380..ba29194aa 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -19,160 +19,160 @@ ~ with this program. If not, see . --> - Loop Habit Tracker - Kebiasaan - Pengaturan - Sunting - Hapus - Arsip - Keluarkan dari arsip - Tambah Kebiasaan - Ganti warna - Kebiasaan ditambahkan. - Kebiasaan dihapus. - Kebiasaan dipulihkan - Tidak ada aksi sebelumnya. - Tidak ada aksi sesudahnya. - Kebiasaan diubah. - Kebiasaan telah dikembalikan. - Kebiasaan diarsipkan. - Kebiasaan dikeluarkan dari arsip. - Keseluruhan - Kekuatan Kebiasaan - Riwayat - Bersihkan - Pertanyaan (Sudahkah Anda ... hari ini?) - Ulangi - kali dalam - hari - Pengingat - Batalkan - Simpan - Rentetan - Anda tidak memiliki Kebiasaan yang aktif - Tekan dan tahan untuk menambah atau menghapus tanda cek - Mati - Nama tidak boleh kosong. - Angka harus positif. - Maksimal satu kali pengulangan dalam satu hari - Buat Kebiasaan - Sunting Kebiasaan - Tanda cek - Tunda - - Selamat datang - Loop Habit Tracker membantu mencatat dan mengelola Kebiasaan baik Anda. - Buat beberapa Kebiasaan baru - Berikan tanda cek setiap kali Anda selesai melakukannya. - Terus lakukan - Kebiasaan yang dilakukan secara konsisten dalam jangka waktu panjang akan mendapatkan tanda bintang penuh. - Catat perkembangan Anda - Detail grafik menampilkan perkembangan Kebiasaanmu dari waktu ke waktu. - 15 menit - 30 menit - 1 jam - 2 jam - 4 jam - 8 jam - 24 jam - Tandai dengan cepat. - Lebih nyaman namun memungkinkan kesalahan. - Durasi tunda sejenak pada pengingat - Berikan rating aplikasi ini di Google Play - Kirimkan umpan balik kepada Developer - Lihat kode aplikasi di GitHub - Perkenalan aplikasi - Tautan - Kebiasaan - Nama - Pengaturan - Durasi tunda sejenak - Sudahkah Anda tahu? - Untuk mengatur urutan, tekan dan tahan judul Kebiasaan lalu tempatkan pada posisi yang Anda inginkan. - Anda dapat melihat tampilan hari dengan merubah posisi menjadi mode landscape. - Hapus Kebiasaan - Kebiasaan ini akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan. - Kebiasaan telah dihapus / tidak ditemukan - Akhir pekan - Senin - Jumat - Seluruh hari dalam satu minggu - Pilih hari - Ekspor (CSV) - Selesai - Hapus - Pilih jam - Pilih menit - Tentang - Penerjemah - Developer - Versi %s - Frekuensi - Cek - Kekuatan - Rentetan terbaik - Rentetan saat ini - Jumlah pengulangan - %d hari terakhir - %d minggu terakhir - %d bulan terakhir - %d tahun terakhir - Seluruh waktu - Setiap hari - Setiap minggu - 2 kali per minggu - 5 kali per minggu - Sesuaikan … - Bantuan & FAQ - Gagal mengekspor data. - Gagal mengimpor data. - File tidak dikenali. - Impor data berhasil. - Ekspor data berhasil. - Impor data - Ekspor data - Mendukung ekspor data dan file dari aplikasi Tickmate, HabitBull atau Rewire. Lihat FAQ untuk informasi lebih lanjut. - Menghasilkan lembar kerja yang dapat dibuka menggunakan aplikasi seperti Microsoft Excel atau OpenOffice Calc. File ini tidak dapat di-impor kembali. - Menghasilkan file yang berisikan seluruh data. File ini dapat di-impor kembali. - Gagal membuat laporan masalah. - Membuat laporan masalah - Troubleshoot - Bantu menerjemahkan aplikasi ini - Mode malam - Gunakan warna hitam pada mode malam - Ganti warna latar abu-abu dengan warna hitam pada mode malam. Mengurangi penggunaan baterai pada layar AMOLED. - Antar muka - Ubah urutan hari - Tampilkan hari dalam urutan terbalik pada layar utama - Hari - Minggu - Bulan - Kuartal - Tahun - Total - - kali dalam - Setiap %d hari - Setiap %d minggu - Setiap %d bulan - Skor - Suara pengingat - Hening - Saring - Sembunyikan yang selesai - Sembunyikan arsip - Jadikan notifikasi lengket - Cegah pemberitahuan dari sapuan. - Perbaiki Basis Data - Basis Data diperbaiki. - Hapus centang - Alih - Tindakan - Kebiasaan - Urutkan - Secara manual - Berdasarkan nama - Berdasarkan Warna - Berdasarkan Skor - Unduh - Ekspor + Loop Habit Tracker + Kebiasaan + Pengaturan + Sunting + Hapus + Arsip + Keluarkan dari arsip + Tambah Kebiasaan + Ganti warna + Kebiasaan ditambahkan. + Kebiasaan dihapus. + Kebiasaan dipulihkan + Tidak ada aksi sebelumnya. + Tidak ada aksi sesudahnya. + Kebiasaan diubah. + Kebiasaan telah dikembalikan. + Kebiasaan diarsipkan. + Kebiasaan dikeluarkan dari arsip. + Ikhtisar + Kekuatan Kebiasaan + Riwayat + Bersihkan + Pertanyaan (Sudahkah Anda ... hari ini?) + Ulangi + kali dalam + hari + Pengingat + Batalkan + Simpan + Rentetan + Anda tidak memiliki Kebiasaan yang aktif + Tekan dan tahan untuk menambah atau menghapus centang + Mati + Nama tidak boleh kosong. + Angka harus positif. + Maksimal satu kali pengulangan dalam satu hari + Buat Kebiasaan + Sunting Kebiasaan + Tanda cek + Tunda + + Selamat datang + Loop Habit Tracker membantu mencatat dan mengelola Kebiasaan baik Anda. + Buat beberapa Kebiasaan baru + Berikan tanda cek setiap kali Anda selesai melakukannya. + Terus lakukan + Kebiasaan yang dilakukan secara konsisten dalam jangka waktu panjang akan mendapatkan tanda bintang penuh. + Lacak perkembangan Anda + Grafik terperinci menampilkan perkembangan Kebiasaanmu dari waktu ke waktu. + 15 menit + 30 menit + 1 jam + 2 jam + 4 jam + 8 jam + 24 jam + Tandai dengan cepat. + Beri tanda cek dengan sekali ketuk bukan tekan-dan-tahan. Lebih nyaman namun memungkinkan kesalahan. + Durasi tunda sejenak pada pengingat + Berikan rating aplikasi ini di Google Play + Kirimkan umpan balik kepada Developer + Lihat kode aplikasi di GitHub + Tampilkan perkenalan aplikasi + Tautan + Kebiasaan + Nama + Pengaturan + Durasi tunda sejenak + Sudahkah Anda tahu? + Untuk mengatur urutan, tekan dan tahan judul Kebiasaan lalu tempatkan pada posisi yang Anda inginkan. + Anda dapat melihat tampilan hari dengan merubah posisi menjadi mode landscape. + Hapus Kebiasaan + Kebiasaan ini akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan. + Kebiasaan telah dihapus / tidak ditemukan + Akhir pekan + Senin - Jumat + Seluruh hari dalam satu minggu + Pilih hari + Ekspor (CSV) + Selesai + Hapus + Pilih jam + Pilih menit + Tentang + Penerjemah + Developer + Versi %s + Frekuensi + Cek + Kekuatan + Rentetan terbaik + Rentetan saat ini + Jumlah pengulangan + %d hari terakhir + %d minggu terakhir + %d bulan terakhir + %d tahun terakhir + Seluruh waktu + Setiap hari + Setiap minggu + 2 kali per minggu + 5 kali per minggu + Sesuaikan … + Bantuan & FAQ + Gagal mengekspor data. + Gagal mengimpor data. + File tidak dikenali. + Impor data berhasil. + Seluruh data berhasil di-ekpor. + Impor data + Ekspor keseluruhan data + Mendukung ekspor data dan berkas dari aplikasi Tickmate, HabitBull atau Rewire. Lihat FAQ untuk informasi lebih lanjut. + Menghasilkan lembar kerja yang dapat dibuka menggunakan aplikasi seperti Microsoft Excel atau OpenOffice Calc. Berkas ini tidak dapat di-impor kembali. + Menghasilkan berkas yang berisikan seluruh data. Berkas ini dapat di-impor kembali. + Gagal membuat laporan masalah. + Membuat laporan masalah + Penyelesaian masalah + Bantu menerjemahkan aplikasi ini + Mode malam + Gunakan warna hitam pada mode malam + Ganti warna latar abu-abu dengan warna hitam pada mode malam. Mengurangi penggunaan baterai pada layar AMOLED. + Antar muka + Ubah urutan hari + Tampilkan hari dalam urutan terbalik pada layar utama + Hari + Minggu + Bulan + Kuartal + Tahun + Total + + kali dalam + Setiap %d hari + Setiap %d minggu + Setiap %d bulan + Skor + Suara pengingat + Hening + Saring + Sembunyikan yang selesai + Sembunyikan arsip + Jadikan notifikasi lengket + Cegah pemberitahuan dari sapuan. + Perbaiki Basis Data + Basis Data diperbaiki. + Hapus centang + Alih + Tindakan + Kebiasaan + Urutkan + Secara manual + Berdasarkan nama + Berdasarkan Warna + Berdasarkan Skor + Unduh + Ekspor diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 88d0dfb06..6e348f65e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -26,17 +26,17 @@ Elimina Archivia Ripristina - Aggiungi + Aggiungi abitudine Cambia colore - Abitudine creata. - Abitudine rimossa. - Abitudine ripristinata. - Niente da annullare. - Niente da ripetere. - Abitudine modificata. - Abitudine ripristinata. - Abitudine archiviata. - Abitudine ripristinata. + Abitudine creata + Abitudine rimossa + Abitudine ripristinata + Niente da annullare + Niente da ripetere + Abitudine modificata + Abitudine ripristinata + Abitudine archiviata + Abitudine ripristinata Panoramica Forza dell\'abitudine Cronologia @@ -65,7 +65,7 @@ Aggiungi qualche nuova abitudine Ogni giorno, dopo aver portato a termine la tua abitudine, spuntala nell\'app. Continua così - Abitudini portate a termine con regolarità per un lungo periodo ti faranno guadagnare una stella intera. + Le abitudini portate a termine regoalrmente per un lungo periodo riceveranno una stella piena. Segui i tuoi progressi Grafici dettagliati ti mostrano come le tue abitudini sono migliorate nel corso del tempo. 15 minuti @@ -76,7 +76,7 @@ 8 ore 24 ore Spunta le ripetizioni velocemente - Più comodo, ma potrebbe causare delle spunte accidentali. + Metti le spunte con un tocco singolo invece che tenendo premuto. Più comodo, ma potrebbe causare delle spunte accidentali. Intervallo di ritardo dei promemoria Valuta quest\'app su Google Play Manda un feedback allo sviluppatore @@ -88,7 +88,7 @@ Impostazioni Snooze Lo sapevi? - Per riordinare la lista, premi e mantieni premuta l\'abitudine e spostala nella posizione desiderata. + Per riordinare le voci, tieni premuto sul nome dell\'abitudine, poi spostala nella posizione corretta. Puoi vedere più giorni mettendo il tuo telefono orizzontale. Elimina abitudine L\'abitudine verrà cancellata definitivamente. Non sarà possibile annullare. @@ -158,8 +158,8 @@ Suono notifica Nessuno Filtra - Nascosti - Nascosti + Nascondi completati + Nascondi archiviati Notifiche non rimuovibili Impedisce di poter rimuovere le notifiche. Ripara database diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 351b00b22..4bd91d80a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -74,6 +74,7 @@ 2 時間 4 時間 8 時間 + 24時間 クリックで繰り返しを切り替え 便利になりますが、間違って切り替えが起こる可能性があります。 リマインダーのスヌーズ間隔 @@ -146,6 +147,7 @@ 四半期 + 合計 回 / %d 日ごと @@ -154,4 +156,7 @@ スコア リマインダー サウンド なし + フィルター + ダウンロード + エクスポート diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a213fa211..7eae85e59 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -48,7 +48,7 @@ 알림 취소 저장 - 길게 이은 기록 + 연속 활성화된 습관이 없습니다. 체크하거나 해제하려면 길게 누르세요. @@ -57,7 +57,7 @@ 하루에 한 번만 반복 가능합니다. 습관 만들기 습관 수정하기 - 선택 + 완료 나중에 환영합니다 @@ -109,7 +109,7 @@ 빈도 체크 강도 - 가장 길게 이은 기록 + 최고 연속 기록 현재 기록 반복한 횟수 이전 %d일 동안 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d6fd34d0d..e7929e66e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -29,7 +29,7 @@ Nieuwe gewoonte Verander kleur Gewoonte aangemaakt. - Gewoonte verwijderd. + Gewoontes verwijderd Gewoontes hersteld Niets om ongedaan te maken. Niets om over te doen. diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index b16128dc0..0f6282c5d 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -39,14 +39,14 @@ Vaner uarkivert Oversikt Vanestyrke - Historie + Logg Fjern Spørsmål (Gjorde du … i dag?) Gjenta ganger på dager Påminnelse - Kast + Forkast Lagr Gjentakelser Du har ingen aktive vaner @@ -77,7 +77,7 @@ 1 døgn Veksl med enkelttrykk Sett på haker med et enkelttrykk i stedet for å tykke og holde. Mer praktisk, men kan forårsake utilsiktede vekslinger. - Snooze-intervall på påminnelser + Slumreintervall på påminnelser Vurdér denne appen på Google Play Send tilbakemelding til utviklerne Vis kildekode på GitHub @@ -86,7 +86,7 @@ Oppførsel Navn Innstillinger - Snooze-intervall + Slumreintervall Visste du at? For å sortere innleggene, trykk og hold på navnet til vanen, deretter dra den til det korrekte stedet. Du kan se flere dager ved å sette telefonen din i landskapsmodus. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 31f815b83..48ec343a7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -75,10 +75,10 @@ 4 godziny 8 godzin 24 godziny - Przełącz powtarzanie przy krótkim naciśnięciu - Wygodniejsze ale może spowodować przypadkowe przełączenia. + Przełącz powtarzanie krótkim naciśnięciem + Wygodniejsze, ale może spowodować przypadkowe przełączenia. Czas drzemki między przypomnieniami - Oceń tą aplikację w Google Play + Oceń tę aplikację w Google Play Prześlij uwagi do programisty Zobacz kod źródłowy na GitHub\'ie Zobacz wprowadzenie do aplikacji diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index f9f3d4cd9..00fe73a25 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -47,7 +47,7 @@ Reamintire Renunță Salvează - Serii + Zile consecutive Nu ai niciun obicei activ. Apasă și ține pentru a bifa sau a debifa Dezactivat @@ -90,6 +90,7 @@ Poți vedea mai multe zile în modul peisaj. Șterge obiceiuri Obiceiurile vor fi șterse permanent. Această acțiune nu este reversibilă. + Obicei şters / negăsit Weekenduri Zile de lucru Orice zi @@ -106,8 +107,8 @@ Frecvență Bifă Putere - Cele mai bune serii - Seria curentă + Cele mai multe zile consecutive + Numărul curent de reușite succesive Număr de repetiții Ultimele %d zile Ultimele %d săptămâni @@ -133,5 +134,24 @@ Generare raport de erori nereușită. Generează raport de erori Depanare + Inversează ordinea zilelor + Zi + Săptămână + Lună + Trimestru + An + Total + dată la + La fiecare %d zile + La fiecare %d săptămâni + La fiecare %d luni + Ascunde cele completate + Ascunde cele arhivate + Debifează + Obicei + Sortează + Manual + După nume + După culoare diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 352e98215..6f026fd2e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -154,7 +154,7 @@ Каждые %d дней Каждые %d недель Каждые %d месяцев - Стабильность + Счет Звук напоминания Без звука Фильтр @@ -172,7 +172,7 @@ Вручную По названию По цвету - По стабильности + По оценке Загрузить Экспортировать diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 50e6e1634..4dce0f2b6 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -19,18 +19,18 @@ ~ with this program. If not, see . --> - Loop Sledilnik Navad + Loop Navade Navade Nastavitve - Spremeni + Uredi Izbriši Arhiviraj Odarhiviraj Dodaj navado Spremeni barvo - Navada ustvarjana. - Navada izbrisana. - Navada obnovljena. + Navada ustvarjena + Navada izbrisana + Navada obnovljena Nič za razveljaviti. Nič za ponovno opraviti. Navada spremenjena. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index dc6217f97..d3428bf83 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -107,7 +107,7 @@ Utvecklare Version %s Frekvens - Avklarat/ej avklarat + Kryssruta Styrka Bästa streak Nuvarande streak diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml new file mode 100644 index 000000000..9e1881972 --- /dev/null +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -0,0 +1,178 @@ + + + + + பழக்க தடப்பாதை + பழக்கங்கள் + அமைப்புகள் + திருத்துக + நீக்கு + காப்பகம் + உயிர்க்க + சேர்க்க + நிறம் மாற்ற + பழக்கம் உருவாக்கப்பட்டது + பழக்கம் நீக்கப்பட்டது + பழக்கம் மீட்கப்பட்டது + மீட்க ஒன்றும் இல்லை + திருத்த எதுவும் இல்லை + பழக்கம் மாற்றப்பட்டது + பழக்கம் திரும்ப பழைய நிலைக்கு மாற்றப்பட்டது + காப்பகப்படுத்தியப் பழக்கம் + பழக்கங்கள் ஆவண காப்பகத்தில் இருந்து நீக்கப் பட்டது + மேற்பார்வை + பழக்கத்தின் வலிமை + வரலாறு + அழி + கேள்வி (இன்று ... செய்தீர்களா?) + மீண்டும் செய்க + காலங்களில் + நாட்கள் + நினைவூட்டல்கள் + நிராகரி + சேமிக்கவும் + சாதனைகள் + நடப்பு பழக்கம் எதுவும் இல்லை + குறிக்க அல்லது குறிப்பை நீக்க அழுத்தி பிடிக்கவும் + வேண்டாம் + பெயர் காலியாக இருக்க கூடாது. + நேர்மறை எண்ணாக இருக்க வேண்டும் (பூஜியத்தை விட அதிகம்). + ஒரு நாளைக்கு அதிகப்பட்சம் ஒரு முறை மீள் நிகழ்வை பெற முடியும் + புதிய பழக்கம் + பழக்கத்தை திருத்த + சரிப்பார்ப்பு குறி + பிறகு + + வருக + இந்த செயலி நல்ல பழக்க வழக்கங்களை துவங்க மற்றும் தொடர உதவுகிறது. + சில புது பழக்கங்களை துவங்கவும்! + தினமும் உங்கள் புதிய பழக்கத்தை முடித்தவுடன் இந்த செயலியில் அதை குறிக்கவும். + மனம் தளராமல் தொடரவும் + தொடர்ச்சியாக செய்யும் பழக்கங்கள் ஒரு முழு நட்சத்திரத்தை பெற்று தரும். + உங்கள் முன்னேற்றத்தை கண்காணிக்கவும் + நாளடைவில் நீங்கள் அடைந்த முன்னேற்றத்தை வரைபடத்தின் மூலம் அறியலாம். + 15 நிமிடங்கள் + 30 நிமிடங்கள் + 1 மணி நேரம் + 2 மணி நேரம் + 4 மணி நேரம் + 8 மணி நேரம் + 24 மணி நேரம் + சிறிய அழுத்தலின் மூலம் தாவு + சரிப் பார்ப்பு குறி யை இட அழுத்தி பிடிப்பதற்கு பதில் ஒரு முறை தட்டலாம். இது முன்னதை விட எளிமையானது. ஆனால் இது தற்செயலான தாவல்களுக்கு வழி வகுக்கும். + எச்சரிகையை தள்ளி வைக்க வேண்டிய நேரம் + Google Play-ல் இந்த செயலியை மதிப்பிட + இந்த செயலியை மேம்படுத்த உங்கள் கருத்துகளை பகிர + இந்த செயலியின் மூல நிரலை GitHub வலைதளத்தில் பார்க்கவும் + இந்த செயலியின் முன்னோட்டத்தை பார்க்க + இணைப்புகள் + செயல்பாடு + பெயர் + அமைப்புகள் + தாமத காலம் + உங்களுக்கு தெரியுமா? + பதிவுகளை மறுசீரைமக்க, தேவையான பழக்க பதிவின் மீது அழுத்தி பிடித்து பின் தேவையான இடத்திற்கு அதை இழுக்கவும். + உங்கள் கைப்பேசியை அகலவாக்கில் வைக்கும்போது இன்னும் அதிக நாட்களை காண முடியும். + பழக்கங்களை நீக்கவும் + பழக்கங்கள் நிரந்தரமாக நீக்கப்படும். இந்த செயலை மீட்டமைக்க இயலாது. + பழக்கம் நீக்கப்பட்டுவிட்டது / காணவில்லை + வார இறுதிகள் + திங்கள் முதல் வெள்ளி வரை + வாரத்தின் எந்த நாளிலும் + நாட்களை தேர்வு செய்யவும் + CSV நிரல் வகையில் ஏற்றுமதி செய்யவும் + முடிந்தது + அழி + மணி நேரங்களை தேர்வு செய்யவும் + நிமிடங்களை தேர்வு செய்யவும் + இதை பற்றி + மொழிப்பெயர்ப்பாளர்கள் + மென்பொருள் ஆசிரியர்கள் + மென்பொருள் பதிப்பு %s + கால இடைவெளி + சரிபார்ப்பு குறி + வலிமை + சிறந்த சாதனைகள் + நடப்பு சாதனை + மீள் நிகழ்வுகளின் எண்ணிக்கை + கடந்த %d நாட்கள் + கடந்த %d வாரங்கள் + கடந்த %d மாதங்கள் + கடந்த %d வருடங்கள் + எல்லா நேரமும் + எல்லா நாளும் + எல்லா வாரமும் + வாரத்திற்கு இரண்டு முறை + வாரத்துக்கு 5 முறை + விருப்பத்திற்கு ஏற்றபடி… + உதவி & அதிகம் கேட்கப்படும் கேள்விகள் + தரவுகளை ஏற்றுமதி செய்ய முடியவில்லை. + தரவை இறக்குமதி செய்ய முடியவில்லை. + இது எந்த வகையான ஆவணம் என்பதை உறுதி செய்ய முடியவில்லை. + பழக்கங்களை வெற்றிகரமாக இறக்குமதி செய்யப்பட்டது. + முழு ஆவணக் காப்பு நகல் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது. + தரவு இறக்குமதி + ஆவணக் காப்பு நகல் முழுமையாக ஏற்றுமதி செய் + இந்த செயலி மூலம் ஆவண காப்பு நகல் முழுவதுமாக ஏற்றுமதி செய்யவும் மற்றும் Tickmate, HabitBull அல்லது Rewire செயலிகள் மூலம் உருவாக்கப்படும் ஆவணங்களும் இந்த செயலியில் பயண்படுத்தலாம் மேலும் தகவல்களுக்கு அதிகம் கேட்கப்படும் கேள்விகளை (FAQ) பார்க்கவும். + உருவாக்கப்பட்ட ஆவணங்களை விரித்தாள் மென்பொருள்களான Microsoft Excel அல்லது OpenOffice Calc மூலம் திறக்கலாம். ஆனால் இவற்றை திரும்ப இறக்குமதி செய்ய முடியாது. + உங்களின் அனைத்து தரவுகளையும் கொண்ட ஒரு ஆவணம் உருவாக்கப்படும். இந்த ஆவணத்தை மீண்டும் இந்த செயலியில் இறக்குமதி செய்யலாம். + செயலி பிழை அறிக்கை உருவாக்க முடியவில்லை. + பிழை அறிக்கை உருவாக்கு + பழுது இடமறிதல் + இந்த செயலியை மற்ற மொழிகளில் மொழிபெயர்க்க உதவி செய்யவும் + இருள் வண்ண பாங்கு + இருள் பாங்கில் முழு கருப்பு நிறத்தை பயண்படுத்து + இதன் மூலம் செயலியில் உள்ள பழுப்பு பின்புலங்கள் நீக்கப்பட்டு முழுவதும் கருப்பு நிற பின்புலங்களாக மாற்றப்படும். இது AMOLED திரை கொண்ட கைப்பேசிகளில் மின்கல பயன்பாட்டை குறைக்கும். + இடைமுகம் + தலைகீழ் வரிசையில் நாட்கள் + பிரதான திரையில் நாட்களை தலை கீழ் வரிசையில் காட்டு + நாள் + வாரம் + மாதம் + காற் பங்கு + வருடம் + மொத்தம் + + நேரத்தை + ஓவ்வொரு %d நாளும் + ஒவ்வொரு %d வாரங்களும் + ஒவ்வொரு %d மாதங்களு + மதிப்பெண்கள் + நினைவூட்டல் சத்தம் + எதுவும் இல்லை + வடிகட்டவும் + மறைத்தல் முடிந்தது + ஆவணக் காப்பை மறைக்கவும் + நினைவூட்டல்களை நிலைத்து நிற்க வை + நினைவூட்டல்களை விரல்களால் தள்ளி விட முடியாத படி செய்கிறது. + தரவு தளத்தை பழுது பார்க்கவும் + தரவுதளம் பழுதடைந்து விட்டது. + சரிப்பார்க்காமல் அப்படியே விடு + தாவு + செயல் + பழக்கம் + வரிசைப்படுத்தவும் + கைமுறை + பெயரின் மூலம் + நிறத்தின் மூலம் + மதிப்பெண்களின் மூலம் + பதிவிறக்கம் + ஏற்றுமதி + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b9f926409..24e40f75d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -19,7 +19,7 @@ ~ with this program. If not, see . --> - Döngü Alışkanlık Takibi + Loop Alışkanlık Takip Alışkanlıklar Ayarlar Düzenle @@ -32,25 +32,25 @@ Alışkanlık silindi. Alışkanlıklar geri getirildi Geri alınacak bir şey yok - Tekrar edilecek bir şey yok - Alışkanlık değişti + Tekrar yapılacak birşey yok. + Alışkanlık değiştirildi Alışkanlık eski haline getirildi Alışkanlık arşivlendi. Alışkanlık arşivden çıkarıldı. - Genel bakış - Alışkanlık dayanımı + Genel Bakış + Alışkanlık gücü Geçmiş Temizle Soru (Bugün ... yaptın mı?) Tekrar - kez - günde + defa / + gün Hatırlatma - İptal + Vazgeç Kaydet - Etkinlikler + Seriler Etkin alışkanlığın yok - Yapıldı veya yapılmadı işareti koymak için uzun basılı tut + İşaretlemek ya da işaret kaldırmak için basılı tut Kapalı Adı boş bırakamazsın. Sayılar sıfırdan büyük olmalı. @@ -61,13 +61,13 @@ Sonra Hoşgeldin - İyi alışkanlıklar edinmek ve devam etmene yardımcı olur. + Loop Alışkanlık Takibi, iyi alışkanlıklar edinmene ve sürdürmene yardımcı olur. Yeni alışkanlıklar oluştur Her gün, alışkanlığını gerçekleştirdikten sonra, uygulamada onay işareti koy. Yapmaya devam et - Uzun bir süre ile sürekli yaptığın alışkanlıkların için tam yıldız kazanacaksın. + Uzun süre düzenli sürdürdüğün alışkanlıkların için bir tam yıldız kazanacaksın. Gelişimini izle - Detaylı grafiklerle, zaman içinde alışkanlıklarını nasıl geliştiğini gör. + Detaylı grafiklerle, zaman içinde alışkanlıklarının nasıl geliştiğini gör. 15 dakika 30 dakika 1 saat @@ -75,26 +75,26 @@ 4 saat 8 saat 24 saat - Daha kısa süre basma ile yapıldı/yapılmadı işaretleme - Daha kullanışlı ama kazara istenmeyen işaretlemeler olabilir + Kısa dokunuşla işaretle + Alışkanlıklarını basılı tutmak yerine tek dokunuşla işaretlemeni sağlar. Kullanımı daha rahattır ama kaza eseri işaretleme yapabilirsin. Hatırlatmalardaki erteleme süresi Google Play\'de uygulamayı oyla Geliştiriciye geri bildirim gönder - Github\'da kaynak kodunu bak + Github\'da kaynak kodunu gör Uygulama tanıtımını göster Bağlantılar Davranış Ad Ayarlar Erteleme süresi - Biliyor musun? - Girdileri yeniden düzenlemek için, alışkanlık adının üstüne bas ve doğru yere sürükle. + Biliyor muydun? + Girdileri sıralamak için, alışkanlık adının üstüne basılı tut ve doğru yere sürükle. Cihazını yatay tutarak daha fazla gün görebilirsin. Alışkanlıkları Sil Alışkanlıklar kalıcı olarak silinecek. Bu eylem geri alınamaz. - Alışkanlık silinmiş yada bulunamadı + Alışkanlık silinmiş ya da bulunamadı Hafta sonları - Pazartesinden Cumaya + Pazartesi-Cuma Haftanın herhangi bir günü Günleri seç CSV olarak dışa aktar @@ -107,10 +107,10 @@ Geliştiriciler Sürüm %s Sıklık - Yapıldı işareti - Dayanım - En iyi etkinlik günü - Bugünkü etkinlik + İşaret + Güç + En uzun seriler + Şimdiki seri Tekrar sayısı Son %d gün Son %d hafta @@ -126,23 +126,23 @@ Dışarı veri aktarımı başarısız. İçeri veri aktarımı başarısız. Dosya tanınamadı. - Alışkanlıklar başarılı içeri aktarıldı. + Alışkanlıklar başarıyla içeri aktarıldı. Tam yedek başarıyla dışarı aktarıldı. - Veri içeri aktar + İçeri veri aktar Tüm yedeği dışarı aktar - Hem bu uygulama tarafından dışarı aktarılmış tam yedekleri, hem de Tickmate, HabitBull veya Rewire tarafından üretilmiş dosyaları destekler. Daha fazla bilgi için SSS bakın. + Hem bu uygulama tarafından dışarı aktarılmış tam yedekleri, hem de Tickmate, HabitBull veya Rewire tarafından üretilmiş dosyaları destekler. Daha fazla bilgi için SSS\'a başvurun. Üretilen dosyalar, Microsoft Excel veya OpenOffice Calc. gibi hesap taplosu uygulamaları ile açılabilir. Bu dosya yeniden içeri aktarılamaz. - Üretilen dosya, tüm verilerini içerir. Bu dosya yeniden içeri aktarılabilir. + Tüm verilerini içeren bir dosya üretir. Bu dosya yeniden içeri aktarılabilir. Hata raporu oluşturulamadı. Hata raporu üret Sorun Giderme Bu uygulamanın çevirisine yardım et Gece kipi Gece kipinde saf siyah kullan - Gece kipinde gri arkaplanını, saf siyah ile değiştir. AMOLED ekranlı cihazlarda pil kullanımını düşür. + Gece kipinde gri arkaplanını, saf siyah ile değiştir. AMOLED ekranlı cihazlarda pil kullanımını azaltabilir. Arayüz Günleri ters sırala - Ana ekranda günleri tersen göster + Ana ekranda günleri tersten göster Gün Hafta Ay @@ -150,7 +150,7 @@ Yıl Tümü - kez + defa / Her %d gün Her %d hafta Her %d ay @@ -161,18 +161,18 @@ Tamamlananları gizle Arşivlenenleri gizle Bildirimleri kalıcı yap - Bildirimin kaydırılarak götürülmesini engelle. - Verıtabanını onar - Verıtabanı onarıldı. - Yapmadım + Bildirimlerin kaydırılarak temizlenmesini engelle. + Veritabanını onar + Veritabanı onarıldı. + İşareti kaldır Değiştir Eylem Alışkanlık Sırala Elle - Ad - Renk - Puan + Ada göre + Renge göre + Puana göre İndir Dışarı aktar diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0937c381a..34c54e0a9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -63,11 +63,11 @@ Ласкаво просимо Трекер звичок Loop допомагає вам розвивати і підтримувати корисні звички. Додайте нові звички - Кожного дня, після виконання вашої звички, поставте пташку в програмі. + Щодня, після виконання вашої звички, ставте пташку в програмі. Продовжуйте в тому ж дусі Постійно дотримувані звички буде відзначено повною зірочкою. Відстежуйте свої успіхи - Деталізовані хвилеписи (діяграми) демонструють, як ваші звички покращилися з часом. + Деталізовані хвилеписи демонструють, як ваші звички покращилися з часом. 15 хвилин 30 хвилин 1 година diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d994b3aca..b9cdf6e8d 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -19,6 +19,160 @@ ~ with this program. If not, see . --> + Trình theo dõi thói quen Loop + Thói quen + Cài đặt + Chỉnh sửa + Xoá + Lưu trữ + Hủy lưu trữ + Thêm thói quen + Thay đổi màu sắc + Thói quen đã được tạo + Thói quen đã bị xóa + Thói quen đã được phục hồi + Không có gì để hoàn tác + Không có gì để khôi phục + Thói quen đã được thay đổi + Thói quen đã huỷ chỉnh sửa + Thói quen đã được lưu trữ + Thói quen đã bị huỷ lưu trữ + Tổng quan + Độ mạnh của thói quen + Lịch sử + Dọn sạch + Câu hỏi (Bạn đã ... hôm nay?) + Lặp lại + times in + ngày + Nhắc nhở + Loại bỏ + Lưu + Mức độ + Bạn có không có thói quen nào đang hoạt động + Nhấn giữ để đánh dấu hoặc bỏ đánh dấu + Tắt + Tên không thể để trống. + Số phải là số dương. + Bạn có thể có tối đa một lặp lại mỗi ngày + Tạo thói quen + Chỉnh sửa thói quen + Kiểm tra + Lúc khác + Chào mừng + Theo dõi thói quen Loop giúp bạn tạo ra và duy trì những thói quen tốt. + Tạo một số thói quen mới + Mỗi ngày, sau khi thực hiện các thói quen của bạn, hãy đánh dấu vào ứng dụng. + Hãy duy trì thói quen + Thói quen thực hiện một cách nhất quán trong một thời gian dài sẽ kiếm được trọn vẹn một ngôi sao. + Theo dõi quá trình của bạn + Đồ thị chi tiết cho bạn thấy các thói quen của bạn được cải thiện như thế nào theo thời gian. + 15 phút + 30 phút + 1 giờ + 2 giờ + 4 giờ + 8 giờ + 24 giờ + Bấm nhanh để chuyển trạng thái + Chỉ cần chạm một lần để đánh dấu thay cho việc nhấn giữ. Tiện lợi hơn nhưng có thể đánh dấu sai. + Khoảng thời gian báo lại lời nhắc + Đánh giá ứng dụng trên Google Play + Gửi phản hồi cho nhà phát triển + Xem mã nguồn trên Github + Xem giới thiệu ứng dụng + Liên kết + Hành vi + Tên + Cài đặt + Khoảng thời gian tạm dừng + Bạn đã biết? + Để sắp xếp lại các mục, nhấn giữ tên thói quen, sau đó kéo tới vị trí chính xác. + Bạn có thể xem thêm ngày bằng cách đặt điện thoại ở chế độ ngang. + Xoá bỏ thói quen + Thói quen sẽ bị xoá vĩnh viễn. Hành động này không thể khôi phục. + Thói quen đã bị xoá hoặc không tìm thấy + Cuối tuần + Thứ 2 đến thứ 6 + Bất kỳ ngày nào trong tuần + Chọn ngày + Xuất dưới dạng CSV + Xong + Dọn sạch + Chọn giờ + Chọn phút + Giới thiệu + Dịch giả + Nhà phát triển + Phiên bản %s + Tần suất + Đánh dấu + Độ mạnh + Duy trì lâu nhất + Số ngày duy trì hiện tại + Số lần lặp + Đã thực hiện được %d ngày + Đã thực hiện được %d tuần + Đã thực hiện được %d tháng + Đã thực hiện được %d năm + Toàn bộ thời gian + Hàng ngày + Hàng tuần + 2 lần một tuần + 5 lần một tuần + Tuỳ chỉnh… + Trợ giúp & Câu hỏi + Xuất dữ liệu thất bại. + Nhập dữ liệu thất bại. + Không xác nhận được file. + Thói quen được nhập thành công. + Xuất bản sao lưu đầy đủ thành công. + Nhập dữ liệu + Xuất toàn bộ sao lưu + Hồ trợ các bản sao lưu đầy đủ được xuất ra bởi ứng dụng, cũng như các file được tạo bởi Tickmate, HabitBull hoặc Rewire. Xem FAQ để biết thêm thông tin. + Các file tạo ra có thể mở bằng các phần mềm bảng tĩnh như Microsoft Excel hoặc OpenOffice Calc. Nhưng file này không thể nhập lại. + Tạo ra một tệp chứa tất cả dữ liệu của bạn. Tệp này có thể nhập lại. + Tạo báo cáo về lỗi. + Tạo báo cáo lỗi + Xử lí sự cố + Giúp dịch ứng dụng + Chế độ ban đêm + Sử dụng màu đen thuần trong chế độ ban đêm + Thay thế nền màu xám bởi màu đen thuần trong chế độ ban đêm. Giảm thiểu việc sử dụng pin của điện thoại có màn hình AMOLED. + Giao diện + Đảo ngược thứ tự của ngày + Hiển thị ngày ngược trên màn hình chính + Ngày + Tuần + Tháng + Quý + Năm + Tổng + lần trong + Mỗi %d ngày + Mỗi %d tuần + Mỗi %d tháng + Điểm + Âm báo + Không có + Lọc + Ẩn mục đã hoàn thành + Ẩn mục đã lưu trữ + Gửi thông báo cố định + Không cho các thông báo bị vuốt ngang mất. + Sửa cơ sở dữ liệu + Cơ sở dữ liệu đã được sửa. + Bỏ đánh dấu + Bật/tắt + Hành động + Thói quen + Sắp xếp + Thủ công + Theo tên + Theo màu sắc + Theo điểm số + Tải về + Xuất dữ liệu ra diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..2ff3651c2 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1976D2 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e94bea9a..90483be90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,4 +204,6 @@ By score Download Export + Change sound, vibration, light and other notification settings + Customize notifications \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 092a373b6..028c02ecb 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -68,6 +68,11 @@ android:title="@string/sticky_notifications" android:summary="@string/sticky_notifications_description"/> + +