From 5021f50e18f710e433b3b970f54cfcf305d1014d Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 14 Apr 2018 08:11:22 -0500 Subject: [PATCH 01/16] Bump version to 1.7.9 (36) --- app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"> From f02c86e61bccc87c0f168d6100f3e1ee34e4ddb2 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 14 Apr 2018 10:19:34 -0500 Subject: [PATCH 02/16] Bump targetSdkVersion to 27 --- app/build.gradle | 15 +++++++-------- build.gradle | 2 ++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4d823549b..cf3fcfdc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'jacoco' apply plugin: 'com.github.triplet.play' android { - compileSdkVersion 25 + compileSdkVersion 27 buildToolsVersion "25.0.2" // signingConfigs { @@ -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/build.gradle b/build.gradle index 7e4c8b79e..cca8d371a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { repositories { jcenter() + maven { url "https://maven.google.com" } } dependencies { @@ -18,5 +19,6 @@ allprojects { repositories { jcenter() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url "https://maven.google.com" } } } From b4a33cba39afb5d3d2b39a00c1424aac8f849e32 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 14 Apr 2018 10:22:31 -0500 Subject: [PATCH 03/16] Add ActiveAndroid source code to our tree and remove content providers ActiveAndroid is not actively maintained anymore and contains code related to that Content Providers that makes the application crash on Android Oreo. --- .../java/com/activeandroid/ActiveAndroid.java | 86 ++++ .../main/java/com/activeandroid/Cache.java | 158 +++++++ .../java/com/activeandroid/Configuration.java | 318 ++++++++++++++ .../com/activeandroid/DatabaseHelper.java | 257 +++++++++++ .../main/java/com/activeandroid/Model.java | 314 ++++++++++++++ .../java/com/activeandroid/ModelInfo.java | 209 +++++++++ .../java/com/activeandroid/TableInfo.java | 124 ++++++ .../com/activeandroid/annotation/Column.java | 110 +++++ .../com/activeandroid/annotation/Table.java | 31 ++ .../java/com/activeandroid/query/Delete.java | 33 ++ .../java/com/activeandroid/query/From.java | 344 +++++++++++++++ .../java/com/activeandroid/query/Join.java | 94 ++++ .../java/com/activeandroid/query/Select.java | 93 ++++ .../java/com/activeandroid/query/Set.java | 103 +++++ .../java/com/activeandroid/query/Sqlable.java | 21 + .../java/com/activeandroid/query/Update.java | 50 +++ .../serializer/BigDecimalSerializer.java | 29 ++ .../serializer/CalendarSerializer.java | 40 ++ .../serializer/FileSerializer.java | 46 ++ .../serializer/SqlDateSerializer.java | 45 ++ .../serializer/TypeSerializer.java | 27 ++ .../serializer/UUIDSerializer.java | 29 ++ .../serializer/UtilDateSerializer.java | 45 ++ .../java/com/activeandroid/util/IOUtils.java | 71 +++ .../main/java/com/activeandroid/util/Log.java | 196 +++++++++ .../util/NaturalOrderComparator.java | 141 ++++++ .../activeandroid/util/ReflectionUtils.java | 110 +++++ .../com/activeandroid/util/SQLiteUtils.java | 406 ++++++++++++++++++ .../com/activeandroid/util/SqlParser.java | 110 +++++ .../com/activeandroid/util/Tokenizer.java | 76 ++++ .../activeandroid/widget/ModelAdapter.java | 57 +++ 31 files changed, 3773 insertions(+) create mode 100644 app/src/main/java/com/activeandroid/ActiveAndroid.java create mode 100644 app/src/main/java/com/activeandroid/Cache.java create mode 100644 app/src/main/java/com/activeandroid/Configuration.java create mode 100644 app/src/main/java/com/activeandroid/DatabaseHelper.java create mode 100644 app/src/main/java/com/activeandroid/Model.java create mode 100644 app/src/main/java/com/activeandroid/ModelInfo.java create mode 100644 app/src/main/java/com/activeandroid/TableInfo.java create mode 100644 app/src/main/java/com/activeandroid/annotation/Column.java create mode 100644 app/src/main/java/com/activeandroid/annotation/Table.java create mode 100644 app/src/main/java/com/activeandroid/query/Delete.java create mode 100644 app/src/main/java/com/activeandroid/query/From.java create mode 100644 app/src/main/java/com/activeandroid/query/Join.java create mode 100644 app/src/main/java/com/activeandroid/query/Select.java create mode 100644 app/src/main/java/com/activeandroid/query/Set.java create mode 100644 app/src/main/java/com/activeandroid/query/Sqlable.java create mode 100644 app/src/main/java/com/activeandroid/query/Update.java create mode 100644 app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/FileSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/TypeSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java create mode 100644 app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java create mode 100644 app/src/main/java/com/activeandroid/util/IOUtils.java create mode 100644 app/src/main/java/com/activeandroid/util/Log.java create mode 100644 app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java create mode 100644 app/src/main/java/com/activeandroid/util/ReflectionUtils.java create mode 100644 app/src/main/java/com/activeandroid/util/SQLiteUtils.java create mode 100644 app/src/main/java/com/activeandroid/util/SqlParser.java create mode 100644 app/src/main/java/com/activeandroid/util/Tokenizer.java create mode 100644 app/src/main/java/com/activeandroid/widget/ModelAdapter.java 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; + } + } +} From 2bfd4a942d1eda4e1effcfae3bd7e77850a1cbbd Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 21 Apr 2018 08:00:00 -0500 Subject: [PATCH 04/16] Remove unused PebbleReceiverTest --- .../uhabits/receivers/PebbleReceiverTest.java | 173 ------------------ 1 file changed, 173 deletions(-) delete mode 100644 app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java 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); - } -} From 5865eb41f7b785c2eb9bdfed4debfcb01ea174f6 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 21 Apr 2018 08:50:53 -0500 Subject: [PATCH 05/16] Update notification for Android Oreo - Add link to notification channel settings - Remove snooze button Closes #400 --- .../activities/settings/SettingsFragment.java | 23 +++++++++ .../notifications/NotificationTray.java | 47 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 5 ++ 4 files changed, 65 insertions(+), 12 deletions(-) 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/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"/> + + Date: Sat, 21 Apr 2018 09:39:30 -0500 Subject: [PATCH 06/16] Add support for adaptive icons Closes #395 --- app/build.gradle | 2 +- app/src/main/ic_launcher-web.png | Bin 41510 -> 50167 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++++ .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3749 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2711 -> 2763 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2239 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6376 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5543 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10255 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 9902 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 14545 -> 14869 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 14915 bytes .../res/values/ic_launcher_background.xml | 4 ++++ 13 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/app/build.gradle b/app/build.gradle index cf3fcfdc5..3ad44b358 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.github.triplet.play' android { compileSdkVersion 27 - buildToolsVersion "25.0.2" + buildToolsVersion "27.0.3" // signingConfigs { // release { diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index ca97cf609499078ab6005186b5158e9217fbd26e..aa3d8dd52c91dbd142fe31e2f32e5a21758ab667 100644 GIT binary patch literal 50167 zcmd3N^T^M||&J;PJTcbIx_1*Yk>VU1#yeMtV&2yz~G7rmI(UZU6v>{)Gc1 z4fJOxVCWD4EV!z3$t>XaYA$Vn+5KPb2Y%)@=G0FNN}}%gOTAlJJ}g~Xx75&GrPh)u zj6t8cl1bMV0f&oRNuLass1kd+!#eO z`|Gdii>qk~OB-7hzC(%w88_EkfZ^PC($JwWlM4F#{ynSC1MzioLdAP0g>J6X&J8!y z7fb7~Jyta=9FRJNTQ}BOWKJh^qA0d#lK7U~%*h;cZ`=Fd-bvNgQEpKd7AfO$>l
;-ghd9h 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 literal 41510 zcmdRVgqHLt3AR!V)2#CZ-N)QD>Y1lxekyJ`T zx}=-2J>&cPD;}#MmCbei{rP!!UvH(|J!l)e%qRR={ez)#tU7Aj%s4SFC(`-<`=?}q z5GMLR7E;rexDLFL*RH;xlhk*FKw10V4Msx$`)-gP?i1hoB~NJaUG>S%txtyN|NTGg z<=S=rz)(J$hfK?r>cOlpzM`s5S8D}tTw|tyT`2)Q&e(SMUA@Ns?`~^7EHmW61Ex&A zcfJ&A8wJAn6H1hT<@5G8$ZA2_1l}tx0$BcHuN^0^l(iBqn}k0lad*%uBJZF~8mW5R z_|*hLYiGed9fb)73Zn(=8wnw|(yKLqBX`)-s!&Tu+gZ?a*7WA=?x-{9UtSZ~6J?oq zLpsqvb+j($5(vSasFd9~4_t5(qQG21D<1L`tRh6~9{)Vuk>Y9>Slc%uZGUmT(7%Z` zT8FCXtZh?2mc;n{lPiq)^K`674gcF1{Us7DTlJvrcjDJxS$Avs-GOLl6$1Xq-+xt` zE9NkJ^1I4d{cW>$zCs!xIY`_?PL*8{F2VU+6rQR-)z~&V8voR81-C z`xRX7*|;&V`D*;GwI#Djb7_UDmh5>KWf7tvGB%&?zf>3{*?)$x1juJ3EP+J$F}$Be zA%h4zoUOqBL% zg)H$TkB^Z;)Y0rD1u;uYZ&JWYc;nvfB z$AV3}vXWEpi!n!%N@nX0qtetq7F{$Hjq)=FTVgQEJ&UYEpLOyhNw;bL;px|I9rF0n z^{*1Qzs$^eMB!yOzShtno~RS#|E5{j7Nw+wCG+L@mi`zi_pn-CTjFG>sT#o~7MYxG4QU7r zn4H++HC$JFX23Sd9{-7OnIy83{o=c#QY`=#Nq|IFXBF}-UqveH3oTn5k=$WSw0Ou2 z`A}9kiJIYsWR+Wo8Qt%D9@mQ!XEIf_w|V%_m0lfEUS_N%n%XqRS)L|1Fv zEnGzIAHK38ELf9iY+ggfG*SrWO!?gcTuvR?&}f)_-TNW+6b^~2sXx}AyL>pnjY~K13t_x>^ED%vT!dXa`|F`2SZQRSY!yo=( zi#KhrpAOd#$XuEo4{LZ86sHVM3@~G2=_JU9fUdO%RIlWlXmepC8!oxdX?(#gqR-n}dQo}e|3DZ+)Zs{ufUJ%v zDcnrsnBk7ZcbEKe8hwkh?bg%8-l`vMDyXi0A$$GI;0>gR65Ldz+Ef7f@M_0_?_Pl+m>OC};hxN_ z3ccDXdnHR}FSqH0#3nx6NXgqn;bg=`n?MhxdtBsIh^8lLYR`3kQFm7KLgb4x35BHH z7CY3`;y$DKO?rh~X5t3Go>J-xYuoX<#x*vp&$CL8rlHa$ibhHV#se@oY{6!pl zAybhb@SpZIt!|nk&BJY|*+R*pX`&Y*Kn@T8r?o=JcX9U(#_{7xCRE9zs!iEEfx0oe z$@5xn?oFX?D(+@BwkBTFQNL{$&|pBgh3Thj4$4FyBk6Plmor4R+JK-S06^mIm!3_= zwqfO8j19+B{i{kD)GE$8ybELQSZ+^Zp`E%Te_B+altokCCn_Z+NB5916Z$oB#-BWQS0RThz9=QpP)9rI&yj~q1H@XoA{=3Kya!%S!Z#wBH5E%>K zSTnkF6dKR3Y+9bu6z8;~Y0DN%cKtw`=G}Nx_2?o87s6 z1Js|91ek;~=TGAq4^`Od|*yKQ!KG(1fx{*Gd1MS!_C=;O4NU{fBQWlqnZ z$Uevkg*;J!LkwAnfxzLrt&UUcPcF7Y`=DF@=8MIJ_iQ^fQjin$U^QbuHT>l!U|KR? z#utqqHtb##jl&ZK_a}dwmGg|rZTG{(7yoRzuuJ6ZC)O+_@LzQ|q`X3~+Wd4ufMMpx zKiRC{q!jd!VYd*({fKJY#rahYTA5$AQ>#BG#eF5zxbqIMprDgvU~Hq=#8K=V&> zby{lU&A^)Chbk#c>ck6@B&rwRHC4Nb;T=BxVNPmht}s)DHAi72HPw%lRSx{O0*}6~ zsIr5a)-|mY9p%F6QL_;jIf0(yL05|mE6p%R&rs%mU+d;->K;?%G&U5{L+i6C?v81? zn;Vr|WIa5jmRsrrf7Nh`G&JogB_#^-!`%4?#A#Fk$7YeS;3(Dl4KXK3$3LgY17h)$ zBvOP*-=lNIW|0zc8I6LksLYSK7OelWZM;wQQO;K_nw$GVLChY~QEN&5H7X+mY@6s| zFzD0~zKd@=oAc^7{C#ApFSe3GVfM`5;{xD;x|eE=Hy9HCVpytm;I^ROFM8SWT=?naIM??P>lsvt2 zQPW$aZTOZ($kU6cAZ0DQsyqhBY%AAfZB*1LNDXIUy$SwLLa=@9)nRW{=d(hWGvRo5 zbb~zhO(I+!08WE?ZU3y9mGmkVARfofe0^oZN9Ucx1JfBGbF>*f%sx`fN!V~9odh0x zp(pOM@q>WfZ?6z18&yWP)H&OlJ`m+4BW4Kc5ELM#Bnm8DL~ebzEzccUD8z|Q|KIj= zV{5F3=R=QfJ6rEm|G8+8p@(uE(zqTxt*p?qlYSsqSOMpR|AIs5iFJh_>Zvg5=UCan zq1mAi`?Svk9#~t8mC$Nr6Cq)J`2*slqIX0dyT1H;n47u`)icu7b0|1jIzB2@owNF<`UrAv**O!_aej|$u9BFr#geRlUG^p(! zXUM%f4kdhWIa-DjQX~XGXQg76;%rMgKl(Slf3-vX&3Ai#L=6sOF$RBj6C{}<2)vzT z?np=8YU6{~(QDcQKoI+xW!?_m6M73{*4(!28;4-404PK?LwUc5W|gsD&LzSb0Ms9rTNEeFC$e4rji-CTX%U*xzxDis(2o zM-kj-W)De!QI1fXPu{S>6Ca&y1O64>>S5-r$D?QY^-bRTq?*{GC&Yn)NMIhcUoUe~ z{s>{h*{CSzZvKt9P;e^$ym&l~!0HB`f((Ktr%B%RFN6}RNSbhXPzksAiBS0InAvNU}(w?>nB7>#}f6%vGjc8L{}V2`bRCqlt+ zCUBTMHu{?TK2e39T#TY-%|Dt&`Y1rxc897n9u~pkTJp>LhULuJ2Aq|g# ziix$T-^3>)ABh}fU-@LW4n9)$`B(bfj&BSSiz!I>oqx=!A%z{%9yuDS1Ezm8RfOqb zSN_6(|KdZ%8ktwT9Ccq~Vjp=VGAIVh*0kuoykb2?rH=M_0q}yKbwdDTPY=S77!WAQ z5Ja_ZsuPp)=iY%PsZWuE3F=7MNGPnCOcesaQA$g;7N#E`#Cdv;KtqOZ%-5*EVY9;E zk^5#(hA~R5wfV*t9Axp!;^?8#s!|{&1JjT;GgcLW!Zw`{6uLnWa8?1j=f-wf z8=Ztkh~}r7c9&R^tvjkg%%4(6Ljl}d9LZD2)1loLNI28o>!~3vs-rwS^FHyN$T$F7 z)ibyCZ!jCD?iq)DPxQOp82Wa~Waabku~67+l4>PjHyC8+@Y%q-Fkt?Tr^FeNG38M< z1wJQ(#xC1)Lnp=Sb_0S~dZJVl1PLP|!w(U&(}de%G(;^K^T4HiT-?5m?v(eBf>h}X zzw&@})F43H>vPr<yOo2QHQiI1E}~1ussVNHu>nJ%2s3#KQ1aBvitazS30#Qpg0H zUOOX?ewKgCz)xUo;3JAk{6#6Dg1Lm`X`3(%)Hws{Fzp~(3A(BT|3ecYdPa`I^%>_}1=`4`a(=Iq6`|qOhwJ+_h|;yEjbjN=K1N(=NoKo1DDt{Y4($jgOGib06%p?Kh%HbXBE z5%+gGvRq|~hT}A?Ibv&rHkt{sW!+cFg& z`ZZPRI!TB%szxe8X}gTQy5_F)B|VW%SEg5QEh`>noUC7-f|FfLUVBEWrl1puVzF1k z=lCGPRgW#Mg+RcO5-;S!{%M+?^;V6B?gwXnc>oh20hZHILz`+(*B5J#|5o!7VJi4i zsx=Shyc4m;n=-qwi!Rf{AVl^*tydWpogA&5-Xki_G6OVuqNa>Ld)m96bPsS~a`d|;{ z?69_g#ENIj@~1t3^POrCQRH4#7o53xky<$nG3&rXl^JU2>%i}T$m$7p*pySRdGC5v zf&j^P{Yd*VTC0tqYiM&N-X!88<&Z9r! zhRAo$TY|A`9(o+F63{Yz?tnVbQFRWgIS{FbroawK?l={30VM#|V`)(AZ1^g~i!WU$ z=N)t9>qg84(F&vF|8D#vU&`AFkmxgtQw~uY?%Hzf1H(X2%ocgvY(RtikpN-k0`!LJ z9P}Lzm~%iH_>TsEUy{snTBq1_mxW&fb?5(p#ym5?Ie<6)#G0=ObTi)5o52s{%OAN_8Yz4FE;`VcmtVee=RF9FoLcg0}uqp#flt#>JzTz~~pPmf6H ztZFq(+eW6t5NK~(w2zF$_g+`H4hY#pR22uNWVEaF;}L5Xrr}Hw`~618*Xa}OnFwiZ z*YS1r83(apS}R>!mFY84AOr-9XtXVs*e&8}Th;}M?-oVYRX%?}2`uSceyDKemG6?$ z7sm4dZRPS{gJoJ-8D-m2k+Wl{Aq>ic;dQFtw}LSIA}gF^UeWpCEGi#5Ujk?WBU`-e-9VS|g$YoOg? zz*WkXjAC6OGCClq^EF5&{I&NbpjPMW1VD*`x=#v0osI?`dPd}N(R^89h%*8V7sgP9 z%JvS$h-a6Hq{t$1Ka0diST!*GIVGBQu^hySQBCcHjWWK;=;8XB$qGT*?vJIUp!UqUL(V(4CFHcquN>*K2}`FlS%c_ zTuL-P2{7+EBB9og0g$I2??c6{DnzWeuiq>ILdjHM#M0tJ6-w@?it_2F(yT2R#J>io z(f*HwiLQ__l6(fhp&U4UK?#)7d#51*_rlLPYYs*#WKhH{(xoNx7LMzJ4=Af;nbD$v z9XIej8Rc9>P}ZF`MMfg?l0VJ5dIxAb_5i|6Ehzs{i2*|K%V8P8YX zI}gFBOaNyvsG6Mz_&e7OzJ33#*3OnZX$kPO8JzSqDvcBkRG*)_a1x%WF(8ioMrl09 zM3pmS3*01p1t$Y`IkG1|8UrT4GZ-a0rUfwXqe2e|fB<^*?sP16!rH7)J_M81>kj+A zMHyt|WWy=y2~}qSmb_Z`QgY79mI9f<@Z4J@5`L?C-z;WMnQD%(%+Kx7@=!EQ3ov0% ze-%62p~)lwH)JZ*{(1=glaw1zKQ{A8@ABwMohhb~u$tdyIamvKO+t1Y;irRh18>xJ4g*hPu`3KTk>1enn{x|CT*WCmc(NB=!x2Cv zv@m+~aMZDeh6j5IMu%VOmJl)$a63f$gdTA5-1}3LT>QIO- zRUY>sv2sbNtg+$;9u195&XjSWoqq6lmsw&}KIfV_$z5G;Ct6*>=n#pf!{9$eKYM%1 z6Mkv?;)}Urpxz$=^VLECmVoAq0|esraF@YquH==ok1Fgl>E2$WQfI+>*$_ZSYDa2;dOsegnL%U- z?l-S=2FCz)0ia6J&%hAP{a9!4r1~woLTp&4=-N8 zb~XJ9x5z=aUhwTYM2de%*rq_TU<+&s8=lfObh_P*k>7aApc9?x{k0|#I3v6kSX-#*U%!ZtjMzz1VVP#4qM!E&G7&ip?A z67K`))f46w%M$eQ9eUWYV&md~N<+xjbLj3HQS7;ONiQiXS`@G$w8v)W)p`rv$)K1B zJQtXl;RhG;Es6I-w)Q{l_`@=eQHDZ&J4y7QgTS}kUmemXKIT~20_y1cN-EkQk2Nh$ z3*ZoZ+PBG-N67b}zTs2Q1QUzkdnWp=0wSXs(i>Ybrq+ay$S~+81BHFb*nuuzZM|m@%WHq!)%MmnBHBVPX zz}8{pieBv8p^YljK=QclnG#E@cv5uKI!dr-E-MtAorUVhjV>Kus%GKw*`3%QZZ6u^ zalp1}BnJ<_>rYr*_@<4faV}a6~Cwl`g$)Kx+J9 zqn7i%sfoz|(ggIAZ|wVs#gSOVy;R%XxG?1x>2#yb+}X55@LG2$-Q=BlmSc&j@;azP zKk~K;3&0ym>4BGS!;`3m-tz3jjqLpmeF#JkpF00=tCOQt5C%dNgaF5gGruJ-TBmsQ zH7WZZ_Q@5TE@kPQ)BLH7vkHwmx-2>Jozcro%F5xFgWxZP!>6+w0_B2Ozm;uoLSsZZWCoBeo8%pT>Ad_D-oK0-gf0f${*uknm;zgxrvf$b{7LGuFp@>wJ!txYS0^%3bMmB>KnH$SZ!)oiYP zetmKMjXLsn9kfY_3f89FYBGsVG5u4DCC&lV6c+dPK8K2J zMC9w%-FQyG^g*zxeD!U(TMnG}9@)rj$Uix=`0*-d#q+LC#j$ON;_WvNDO`|{21yxf z7crwuksoX%ttyKe9E0yg|3&`jL<*4v?XB6eCk|sGH_3)bU^Df{^fwoUaa1Q4WKqTA< z;%j|`|G=29%_j%is|EEi{T+?1olzn15Yt*4^n8BW7Sf5-47E#wh=Y}) z%<;Rcl?7i-F+APKu6>cr^6k9$E)9GBP!l{%z4qfL3b$r!C(-&gqP^>1jw|lyM!L^} zoVgXn|6Da1YG;ElG84|lB~N6B$3`FJ>&k>@?cV?yWq=wit)v;U^K3uaeDJ=>8~MvU z3G$A5KBfp|Dst!Sv8kHsZ=bL~`PyCg?>B1Q;6B*71S6gI*?p+&rYpd8BB zCD;bgHu-qEAl{~YVsO2!{NzOQqk&&f@1Li3o{gl)E{c6hATSN1tAV7oXkCppz|ZFV znT+?Yh2`>p>ZQkZB{}c0jtvZG|SC91bDR^epB{cnF_D2lcxd< z(g=pi3(}6fC;Z^xd{>!F9T^*s6?Oa_n_J7uSoAdJi%`o_fu)?>cb|nEW1x6j9HU))Jl{5=#Fb zW}QO0YC@aw2g64@JRphDm&AHc9B7HbIWdCX0(M&7_BCE9xwFj$2Ff_+TMq(V&Ot21 zS%=eMASR6w)D3PZrG_QEN_wk+d6~~Uy}WsCagDi_nN#`P?xitc5Z=G=r-Lr=fdlt zLZ*F}D0YuNQ5AN0#>W@Zgou@$cRgj6JulVT)-@!<4^-Z5Do5QU9oj)oqxgwan?AYc zZ!~~yfkvL)yuoZYG~GvY5~A+)*9pB|bWiQHx?AJv#_tXXPD$=#yOgS1iOKYkOko#OGYcye5*^V>wXamn@7G z4_rA>mct&;zEu!|ZyigXRCg|UD-fex3a(qq=il%gZS;!tf;!d${U=Bw1vl8XP;e>8 ze&o}uipotsArfdAs6t7B259($yH`Rv+*SOu>)vL5Cnhb?fS&rY(Ex}Jc&kN;SzpJw<#4ZcWM$oVCW`MH%b zd|QM)faFdkuD4J;PlyD|)IG3{`h8(*THDx8Y(puyK5iB%hhKi%8|;H1O=@a-3b-Rw z*mOj|*(llr7q+)rhp(8ryl($Bo)(r)u@d+dI=B#`njMQ&rYfYF1L)y?#358$wD=Vp z-%F7bH1i;koep6%aZfFX(7bv?nAwoFbM|5Y)V3&qm=@eEp5!VQR%6I_3S!?~e&cJ7 z-bC1li#jqwg*%=tLr=ANU|x<)$^wlKV}aZA-UJ^W2GY?R?ibo`Xq8iPU-!rcYyTrd z1KyE9ke&%a{v*C5Yf}8w+?vZEy!FLf%wNtA5e7Cb+9Py%W_19Yy8$4ShEoI?`qHPi zU;1UQ1LuJzp=&Hs(#uJMGGxxzTlH@2$meoVS-EmhIbx^;s9=rp323G`bow!fc&1Ul zYm&>Bx$FrzYgBv7qsJ2FyCB5Tc>5_=7M>1jxalA*wfngVe?%Cas#W%hv}p`ZX(3F4 z_k&XQ95?f&dzpZKelKG{=wMl#)+pucotSH_URH;`{G*j ztFlunzlDu!@9;}>FLBeC8mdz%+}zi@GE(nnXhrQRI{N*z*kJ*}zGeA?=5v>9iQ_U` z6G+R&ob?ipX4#>Jhw8xPWI1$A$w$8N1Uc#Xn%G1wYS45`XKlvpZQ^h(NR9tfe%+ z@t#qVvg}d3z8-i8k~P^?(%+Y6Xg8~KaqgDfFB9Y1nTd9dhKY$zu-Y=;P(iwYbSz0S~YT)is> zHU~*eyau$s+ycT5>@H_SM%XYypqO<|54n?zDK z-v!#F=maH)18n5a{4X(AmtWZyq-ypaH;K)jJ0TjY=95w77e5))x-e%=2z~c#8M@&g^?kBD&8@ilQkM5G}bGYI9OP~uwj;W5E|v0hV334FSv^j4Cr zCZE&}!+i)*SRt3NGbIA4d#;RL)u_WhIs`R*z|Xn3(S^PGl<3knsrFNN?6jW8o$|m(Wo}H^eWy(i-+%2ZZ9^`W`s8jIg&e7v1Lm<9m7NB_rUCSVXe-HQgH28A zRPV(rqXmhvGyfdphA*%Q!Tc?HHnWLZYhg^VE_##gmoT<=`*rx`i!Y10_}d06;+>=# zX74$A3f3!JG_cNfou85=649|Y)+nlBfOQ!oO~%w$0RWecTc!tD8QV1@{CZ}{NfU6} zTq~^Ufe2e?xn!xAkf|qROmV;=Wa5CNJH+t6c1$d#d!*%V1aNew41DGS&H<|hLX|y^ zFq*#iph@7#%YOcN^2|wGU`F~%Gw;gu`wMA(6u4f7`;0TH5f;N^hZT7Uk%_a>W;n6h zMFZK6mVkO08kfV|p{H;YJkf)8A9X}OUXzpJKYMFm3o!oP;@qVTqGejCJ0G(dsQvYZF{$qJQ}?W|&PdQ%F8D6!A#d??lIc5TKo|jRk$Mwd zY?Gk!ud5|CW!N7)zv}P89DJ77^el1gpJL?6DVLfpfl;9h;XG?QBJfKKAAjdeaFPY2 zr-*KQzd>trzi!K?)dH3=;L6~p2C!KJ{1Eh9Iz}6^^y)pAU3XGc!mUXEOP{;)3=00v0O0X0Xc0qifeUCE~2GcWl`zm=Cf z;%(gs`Q2DV$0q_i*pI30#uL3u%NAV5H5h19p9g#WE@*t`j|(fa;!OaTk7Lh(3ncJ@ zgUYZ|6y=Va?VAaA$P??iO6K|z&-1gK9@f~z%aPm#r6;4!dZT5xEt9SY-=_o~Ut%g7 z@l}++G(0#{Qif%#2qy%ELanD`TfD5==4Fx^&3OH)!eu$JcD@T9bnNRqnE7B92^-OC z-2)yQU>iRK#NaS!oF0unChGiNv$LSK+$RvtW_hO%;?X*LH)Je1A(CZnyLlV&u{cHT$dC(d=9ASZ`SlS`Sj89Wwyn%#s^b zrjBl8vU^MEdgMpjm|4~mJE1ZCa@PsX?reYtg^~4JfUpT@58_>(0km~lRiV{kJdGM$ zlq~%--fI|mzb%2MAvVRk_AJO9)%Eb$Prv=X>zOMdwmU z4c|i>4>+rPA9$W|kPKQi>OXl_&`p8DBEE+oYU zVTBAEp&L4CtI7?XW}YGR=AvfnR+HwB#Op4WUGRy;_&k18D32XYb6ClPequQ4PV2ai zI}wn}rSJ6;a|~ra7hSp6BnP{Np)~i}IilMakA%)sKcB@(zPKZ5_*&|xxDekzjODk) zd+jp7ChD&i7y7W#u8kAL3L@Tlly;IiI)`$AEEOi8amr{0c|cPjf~qE9Uf9LN;d zb9C+|dvQ)jkHLph=^G|hp~#I1=}TT}5b7yA2GCKFS=vYbLyGK1H!jI%H@e0Nf)<~G zuiMQ+-lt4Gxwt}83Wd3V0@7t0&6ovWc2)M5JfAQplX%(wDNQIwQ4`OS10cl6hrIUh zHww&RsglwA^1wqGS%6mG8q)Eaa5T5y6~44`*Xh@7zZ6@ZDOwbB_+>4bkgj?F z#y1sX4jGEmD~@i-VFdks3k?bz@~w#xCcop0Uq;C90p-IUV8VHUtLKva2BDJ-838fm za^k(D+pkx_E8drfL>yjt{?PAQy4I}sx*=@aP>qEEi-w z3A{HNmswjFsYOogQ=y6X9hl7sDV!LK-E%Ilv-2JCYNYJ1OGYO+s!JW|Ap-8KLZ}l@ z3zQCB8bfZTgvmzPROEH}Ee$$K-`02TA~ZY@^E)q?Syp!Z;12@!>(%gzFP%W<^r=7a zqOy!5-F4sOnf#Tv?{*8`T43=mMJ`C8mlW*M7u(;0h|+?DAI@pN(?tHqYiIg^7Y;#F z)1Xx{ZBxAT=x}bZIvm)};&qMD+i}m;w5ge{#M^`t8v<=9K97e84&63qG;)4%%W?Q6 z|D!;vBoFJEq*;*3M zBW06WtX@#}J#66fWQ`qW>KRJu-{Yg+K-^|gh1PdPt?HIsbe*dGk?gyrR>$` zTtLABArxmz7c_j%BLPyr8?;i6?hL^vwfx042%Y`wojxcsf!p^NIO-uwUf2HG=hkOW zVa7I5Lfy7D`>ZaY5$(kX>`N0)PI(zI!Y|^R4)}&W(Z7D=CVcjNXih)xvJ8Y^94mSs zuBivFt3~q8ON&&Jz19G<41mM8$;0lW2W;ZIgod=a*aFJ1blM#mRcv_x@|pOF3w zN47lsSuoJyT!~XIiF}QCz(9Z9j80t`!%jK4!frmW)QbMXvkbB~Y}bZi#2L z+1)5ve!@}M2EyW;=onc!xRP1`%8-Ayb3^_?cQ= zzKE&Dw3<^RM?-m31~FaHQlfo3h!-rrPyL0vpOIo&bq$0!jHcUmL|rbP?wzw1E98t5 z;N&)hnp6z&oSQ#a0MgH6cYRo5GkSgShfUkv)dN*KfqRGjeMuB6h`@p$445~casOK5 zbT4X`yl}OA%!^k}t9-^f0B$+yyP@IH>nOq^PMs&%n@Vv3IJ&<);-xQ48ECN7w65nu zP+%*`B7XejLn=bi3kHD6V?p3ude`&ntGIif zm5B?!r>qGu(SzQap*u|&%sVgN_J0b9UCa3*%9s@NSj=Sw<6v{y68dDZ) zo&kcdk{S8+xX`?-#n~=DY(v0co9p`|%cXSz0-$O}9XK+lIs`m#*o?OIK`G|VfQmNTJs#T~2pvnOTmz$N0L^H*`*(`)Os zggiI(n>|(s)fi&jS?VmARap=MnRDOp6 zxW!=6KZ0RixcxqGD**h;9gO`#0-rAd9nlcquvD4ar$nnrr-+p0Yy0cqqHkFp*ebQ1 z8PqgS_I~K}SQ1a*=HbRVLi9ds!*Uf9Z7sj=$efi;(WYV!EP(S>U1LLb_>PKl@VgzE zPjpDW*B_5T-QLP!cNvkS%(YDK?;FK1_y!Qxs@z^ukMktnT~^`(3m=k+jW| z#SAf#6`p3FRpnsrC#8y)UT?fs?h&%hzN{Z@x$PVLRa1p(>BFf1Mri(?9oNDcX(EEo zLeaQHXOL!<;r&I?MZ1ZWT;`Tr)rA(npV_E`k)6^VVJ>Y`jD_p9j-1P-wD0nO&Ii~x zY^1O50oA*G;dyOG0>IS|G2DBG_B9raU>a&*#HKw! zLIH9JQZW*Mni5~Tr6|6&@9YjRfYtCnJi{;R0`%YTyY0x7FC!Biq z^W1RLKZOb(0I#Be>RUi%G7wuqUYShd))$)7x;t~@hm~E-VdK#-Ya;17sx;qCk5H9* z-z9|qjFl#LGi-1FgkhhRcrGLre!>6aZ9!#GmfnXFwX^31uE=s+V$K6axiz2#hy$f;2y!^HH1o|mmk+xwlba{V%}e%3NMUuJh=(~ z9e3N(W)#SzqA2=+TWxdt+scNW3iJ9Rg_fP-^PC_TsFgI480}l;0(uaIVoYW}{zR+& zA$I&<5Xg?HTHyvzp_BP6_P=u7D{FK-7!kXv3E&hE$)T5=1E>Tmd$5^w3vO>eFbW#9 zUn^R3vSs~45B%)ZGqW);R`#cq*uYe`2dQW|;Jr|J8Vl_XHd)V9nWJg}W>a^;Ls%+o zFbIAKK#j_uayKND`Zc+doedLCTp%~D|-SWY!EkG^7weG(MNpfHT7UXi8# zR`WBmVYwOm=H;xu=d!TFhsm}KGp(g%0c z_M@9R*xtOZwX+TX|J3&a#ba%ND~`DE`z*M&hH6lIGa;PUr?f`tH>C=qwC^ zC_t9^Wv=7Y4}trckBj}+z{7!g>Z%hAQ^fNv-fGB8(N6aCNSywv+J8LAneg9aHo%t^ zQY2L*fM!pn7C>?@bJTsJVnqgvSKY`+5m{N@pH8QKa5<;Y`Ne2f(K!{-JvXU|9js+` zfIeH>@;9pAF%fwuXiPs)z}=}`9{7MKc6VBqXI0GP98bQBT2hiWafHRVo z@=AvDxfBCemhk^ry6!-z|NsBKv$IKNoIOKk5_ggiLW5+MjIu{WxI?nDvNDqyLPeP; znb{q$T-)s@Q~vGU`1i6FG*Sy;%T`DBb|4 z0Z8%vhz++nd4V`yHG!=d3lv2&M>oS_Q}%|m>*g0P?vS$u?A^VT`$!BqG^wP^I;h$6ZU)rJHVw)j#2Hx3Ben%Jh|L;M1h6k zPjJ;4Q!St7M-3Nc$uSVD%aN??&;C0cZR@42f@)%f+-izShjhZTDUnKgOp5xV1({71 z1z<(G9gn8$#I>8P%93RL9^}$d$j%(au8ZZmOXj|=8sdE-RiH}^_~sMYB;d;`4S5oL z19xqx2oi2olM&y@7+BZIipgO!$0Dds<6Mb{|-*5Pl zA*NwQy?rtjQY3<0C!Y+OlcIS7yKdTZ8}kozULZ#~^wFYv&tdNWp};_pr58$%8^7_O z`lqVz1V0L-fX^tZo&pGfhY7e4jrmP9Z4H5x@#{16J52`bd+p0ZQNIN<$!xU0kmZhl zhgDu-g@C{D*dF*yt=o}A`@OXZ6;*@BAmZ%b&uaqYRl=C< z=hkPodfEIzliG(e^a~WtR1JW;&skRV3MlRY3!W-6bKeX-TjO2a zv_T*4MQ#MG(|*~-6ji{#ckU)YRp#OBO>=gTQ1<2(r(ahLhm|)!z%d@b6_kGO`amFT zN2(n-LShUOF^>KEgOshCd>KLYlXI7B$+=-+lEFDM6qpxh>v_R#=Dz)RUF=O0n$pwv z+QSr|jVnPJ=Q6B$>+-iWKnH#jN>CGmf9`xw`)j!*J>vyR2uO>3#|Aabb; zBPN%jJQ4~F)oClW`R^*yCNd@r9KvA(-D1F>F=l|b_+&y=#tH#nuC2ZMFY+5J0H!Nl1exd z1_-q9<}f%4ip!4$JI&Q$+bnEADw_bnA|mod=0Y@EVd2Haj0Ht$7_`Y~ao{k!z9^oq znO~X=MS-b1)#rwz_#wL{mD+HdL2l~;anc|oVhfSv6QehBYD4<;!(n2&1MRQjvBw=1P+joI1 z5%Y5xcTq@1;<3NeJ-$%ebO37&TuT^$`ZXX{2;}lDDI?rY$Wg_Yf5J+^+;dawKowzFRx=xepuUA|Pur(eu8fH7lpJPSwa%mS2){?i{hkxI2_{6Ht#j5U3tM~k7R zBFC_F;y7G?kyqZvC|Yquv0!K^F!s$Kng82VW4~8tRrfDh>Gae9qn7Va3YOav>COaj zU0?BHW9no>QH&i5DWk7Ui`m?z1}X&6%27a|^LVe^qkcLK)?CS03|@-w+-`~?@S7*W zfI*_Q)>(70>+w0@%*U!ZtXAhRp4G#v7>>I+RiQ__;8Ns0AYw7)XY^AGIJB+qfb56h^R&r+)7JpU zRp~*YVoq4@qf}V=EJgE$9}2PXVMyj;X|*uwA_vb(qE>^L0O|9<`ziYaz@u9dNMxM8 z*BVw0{=UEaqCz~=BXW?sm~V?*OdLRw5a^@qqUpPFNg*xTQCAEbN#EYE19yvoG#7K= z7=BU@)u(b!K7YKIo?Qv|c@I#pT&pIFfcU7DQfpBH(q*&|-ot^iHxF*ZV$5BRxkQfY zYEOaw6--R|t#0oBT8x4ZT^+*;SiXP_I8=QFwuN_n-$U)vqRb+mk&9=_f%v|W#t6lI z=P# z0~99QlK>Xo;WjhRpjOybA>h0oNty+=Pl8Cdzym6&4?uA%WJg2H1BY48{O7^XXA2Dv z#{=u5x`78}WLA<7s4~~Jyt)x+w5ndIFV3h{r7q}H-B0?N5*xr83F)>P2duJH2=ET| zgE3{zWt~SxC|&0JSWfYV9-C!ik%ZW#8zRgoKEai@AL@4CGl5pXH5q;Y&JRS11HYhY zOwwx3j2hFsG#J+;hNOTn1!;q+%DCb&z36k8Y_G_=EeN3g-&GEqJtJ+Xdv3m#vjBW^ zzF&W~e;m#}JRI?w4G8)2ga_tr0*J*DPGma~@ZV}nqI>We)&#&&{{utpM92alO9naLTj8k>vvfBv^kb-XKeu>E`7MB15vIxCP1yFY|e+m~ZuKk>i z8e{4Q8-T*HM|9yDz!e!lVyL)1?7@7_GKC%t(kfDmg*JJsMG|I%Y48OoC$j@C zPa7cdae$(h?y{74_`=qelGL;xekfhE`4azULRDT}n-takmV2)oTKYw)3SV`t+_rAgD zNp$qf+R6_Y{nbPltIydm0P;|lHRQ|4mt6(szy88sfRA7&+tCGZZX4;OLc1dX0-Z^u zU#_Wwy7ei96CL-PmaH2}@>sP*HKzUcP_ z_+Xl85(8Xu$8)9DDJaTZntPWzIo(LNtL^Skyzy7i+3yz3cTf2^1@3Fn%CL7WM?+E~ z4{gO`&jF2%qu7chsA=~^^^jf&TCn)^kouQl7Idl7v8Ij+a}=mcP%(pv2Gc>_Km|ka z0}|Wlr~C^0?}D9f&J88_op+z*Z==Q#LbUTA9Xqo~q*?ztrwz+mvEn@ky=N+cj_AAL z2UQq8EV(fSF43%0Bi7)ZneWy2>F8zs0U=|U&D_D34CJF#-$Yu6 z%e6J|BK_uFt^+?QSV{a(xSTf<8Y*sJU z$|S=|W?daf^^plBFfSIX3xqqXdYWHea;mZPPpmBf7@qs4RU*Xv;Y77Kbt)W1PL!oX%Qs~p=>#A>@ zqNBB~U`DntE@I;8^k`qwn^cST#V`K}`8zo}yVBvWU6};F;`Xo7V%o zU_gul2B8z*vY*4un0$EHLT^MeZY~dZ66~IRMie;BcugPTDj2D-eHIN-~k~#U%>Zz}&BWo8dpO-0TJq zh;ENo$bP?*{|XRegv~SnHp+0yCYHfW(qrj=y9vJM-7WGwSJU)Gz1G}J_EMVnmTB$9 zvFAJQE9%E1uj2_l6VLBoURrz<#P28U!%MRv8=M2o+D1_YpS1Smo_*G&D)lg%vO}nL>unnUN z4ovl{q`%yYN46~1uBaVc5jW}-xI~%pg@PVq8FN=5fXwEV*h>-76TNKqf<)~0q&x1q zO{p`W(%E|K6leUb_*WK1X!2ftsO)^^gDD(Dd`RMXx$DT< zq@(!XEAnApfI{p*)GY_&nm)Qt>~d|_BT}X*AMGdJf3Ii}`j}e~R%3i~t7cyY$;X}$ zBY@!zVGP0vJGZC2#9`7k2o@ z^bMysc4WD|`;e|h5zH7d5wY60?$fRJ*8?>dYJnDV9SGC43#Z197LP8=EJ#QTLZ*@) z+bPl^y&%5&SZ~ZX+p(@!Y}T=myG2IfEQPRBm)s(HL`=TSm3mKDo7E4ZkR1lDOnrt8 zcw5y36S5F!k>ZG3catTCqmO{&h$Fy}e;X)%+D1+dZ>E9SJfgf;`%gP|Sq3jAEm+$P zq(3qH!=`2Ftj~TZ`7<^07r_19qgfyj5N_-oLXm=aAN};|d{v%${`t+cTUr@zCTaAT zI=K)(bsQij3rGk+(Pd3%PUOI$ZOWbqYQv8;SJ{sh@tz9vmWGKOb-58j2!yRdY_17+ z{w^8d@zTY+J@)c~i$WJKC&LtoJq89T63N8wACBfp0N&T5g{D*xsoz}bS zLQmN5K~w4^e^BQN@&X(%sbF>#TTr%~{xG_2>3jUVFL@bPj>c!hDHDsRy>AQkDh~2L zJP}hi9uD%Ie8z<0y%X2ASkmQ)toSEppu?$MipK%iX~1Cr3q{gO0owAdWK0|VG9AeW zmSF?&M*#JlO6j?4Kqfn^fcyjIx(uLsHomd)e5axv#`;gr{Yhb8{I83<`umT6EVR2Q z%PW#Sv~WkDU+pc~9?uR(Nyb@Xy>c3Ol;d7`D@X%!DZf%O)<6?2xlY`lvG-_SQQp$B zi=A;mkh251kr;4mZ<2D?*9bVzNkR8Qv4(;eHr3iDgyln(S|5sS=Gksz3u;0$m}PD?;9Wf_I%o0qAjsO+E3dK;Za zgLy-so|zz!V*~{u6DG__aW9$iqg_Pzl{ekGK`@)#@Qv1mmr?cW(pFgQck+!uJ3B)X z1(OolP4g}QLtyslYaf;D7yotK@qp{p-{hkTvA@o~uK?~B$$9{VLDkgH_SWF{2Nj!T zO3Er=?bJ4xDNvlrj+bQRX;8@V>hGEJ?64jfcl`p7u>i@UI@R&7Tr89%7Jo2>Dn4*cab=I$*TlAVkXNe{axDt-7aSNFsc1o8s9-8E_lG*a>l3);UxKI$&x89;>d;5=R z0PSx;9?a;qE-+`(Tp9mq7(bKfX~3D=*Qx(`smBFknV95f&3*x5hR2@eDHdK0GlQ&I zOYn<@BC>gWBU~#xRL0(!-5Zo&LoaQwfcJgcpJi0#bkmPv(<#}pxBC-fy7r#P4rf`a|{Nesciwn(9=!81qm2d>xd|V~x z8Ayh$R{5nU(qr?G!BXa=RO`uW{hHwU2FEl@< z2BLL=`zO2A`&FEx9%i)&TBcOpY5Y^=+N`3UQNzV5$$ZyVmb*WbF4N}P9+5>*0FV|b zTs62__`Mw|sAQKVz-5caeJ571=a3jv$J1zbeMLgXNlo#dMuw3W0Rvgs4E|=bO;i2n z*V50{JS|K(YY@}sB}~wJhXDWyw}s{UqgH2zyIe7s19FO&Cz}v z1;e_1O(dmyc9EZ`uDpB5G>`{^$CS;7A(W^&28<}=MSnN~-j4)x3%8;MzgJo)(=(|a z6$^0Za#++8F%2he{z)4pDOuDyLUqOGA%sIG-=h;ESi6w-82hsQ6dMFSy$9&a`hrLA z+%B|e4S}}=L8<-(BW%2uA2z<(* zbR=krsCaEOJ+{6+acC{1NG1I*5cWzhq(+Yx8mffTp77A_+&B%3A})4(>fwLivzD_# za%cek_Htm7Dqdwe?^XUVG!U}r@DW$EYK!8MDVuMy&|}g7cQe76;fooAaWit#(xY!- zX_~71J3Pj6P3B)Sdf|@vj49Zd6zZ!OEwFoZ`eHSsQRsAEtu1J+hhu$0@Irb9SjW`} zrg~0;p-MDEpd5$o3FnFY`%NX==q$?Sap=m*1#?T7D>DmkTjH7B+7=e-(i-}Yx zzXF-38gmS%K_7WE%_rf9(G+5YF!l^eXq``>g`D4nomTYJC6%JcrakhYgRLicd`{QP z$JD1bH6a8&CYT#Nw~RYL(3t_2b;uA_pxi!;gBxb14Fzs>NcZ5{Pjoi9R98H}=UUb= z^@diW2YDt@J;BG(146)=A0sp#W&4E8vtPTjuAFUcq=P5cY2=+4e&^N{G1W&3qw)P> zQl{l*A)ySISxQtsXy;5$r=SFX4dr!NB+qjmk+sEiC`PBzol~}n^KF=u!KK1X#CQuY3)-GaiC#Bz0bRbT+rTfX;e4E_J zfk0EOfw~U7lI&OFeF0~uHdy7mP8ft=7J(tG=zEmllwWxWpVj%-;L>uVcCcsk6Iz~z zIFo$}Go#~(fXm9^%W2-r&5DJ0FnwthmBMfoNu4{kXygPh)+` zEv+!BM5aWz7RqxKZMq{9(W1k5O{gbfJBViwhb zRh|p?i%J{W%h~KM+6qZMu6}y^-3pbB7Ha+Wj|E7l!yHeB zVV}VUw}5)OAnRuf2Qn5IHeo9a#jbB1i7&~&+@f0K&K@XpZV@yFkBs<$H7cXph4|5B z?fAIpzOY_Qz=fINFzvua7CR*>QuQIVGMD2+?uOVX%spdyM5lGvAfZ0+$(9O^AH8_s zW_+p)5fAp-ku8vi{D-@|oQs~LhxZL{a`tCP)F3^*P_#j}}^t$5_!CcM3ggzAx7 z5V4=7k)%BFf(SJnBD6KO;E52Qz-~qF;wYMZ zZVml@`2c%=7D83|j+2z-cJiT0FzKyz-Qe-7c8FxNLD~M1*8>^)7aPUR)f86f_@S^~T z&IhL&*b#5Je~4L|D?1L;SztE1;IG;W6US7gnXFsi{2cB4k|PYc_#B#u0yyeLm++Hd zLSxUrxXUNC$)?L*xF^%00lPb<-DbOtD+&omgdE`qC-vD|1a>S*gVffz^-GyBI?8g0 zmD!s^)05Ak-)})edFjD zE}Po36#a(9x-E?Rs4KUtHj|p81zYyl6IF)-pd+jC&|}9n3yZ9FXxz}^nYEtgHhA0V z>Ch-h`DF<-dcPpkiT%3*GO+%+b9?1y5g0p%t~l2Yu12+k61IeX`8mw}o;l0{ z-{?&d>E?pvri(DeS)cea)^7nOa4_jQ%~PIKyn_`OvtrnPLTJZUH!NWXcG>5xTeen>!yK*#$>@Oc zbLg$@Idt#wKGuU^m`Mr;-0$*1K+@d0;Z~R|g{SESL};V4`mtii5*NI!H+aq0TBQHY zcwZ{nt8`3Eo&YevmEw=ydMeu4W`5F|TJ>MzeJue>ij+u~3z1&mtFWZm@)T9)vzLBT z08)u&`<30Q=9kzz#4_HWJVe?Pwm7LeK&aO!1^*TG3s}Em(?(hTZZK|-Tv}IIk$i|& zP|BX_B#JsQWRNaJdAy6nNg$XvxMr8bhGjwddRp6T2n?lo#AuH(oM! zNqX#^{6^xAMHLT~h0MNgFhZx^1n=kVgV-CJ=;su3XpOubWIQ&b8%QSqgz3?tEQC$g zh|6a5!}Akf=WGg%iMlacQ)ylWg|bCrk#?V9gi^^A85)i+KpAysyYFns)~DXTf>3Z~ znFIDc=)jJt>NfEH_Ycb& z`?Ab)Irnisc$u_x=@aH7rV<8LPYJ5cC64u36vrZGfy6V z6K0tL_3@zJG_gW@5^SiaYnG-OVt5cb1-Lp<_b4fn|B0BoEfF`c(Wdk)od1jRCEkXss8za@-w0ZZX=sG{tf}+dm@g;KKg}m}_Z1axHV?o?; z$I*Ko8Yl|in+B^^e7lh+f7k_}$rMwd4)}6U0X0*4c?+6&u=%>#yGDex$jCWAo)sP> z3{M*sA0``g8dAR_pN);@CyzQ%Q#|Qc-yO!&p5<|~ZVwXtW0vc;Ey493B<=v0p-Ze_ z$|v0|D>}1M=Qc3OFU89D+0(&>t_(2u)EZ_7%qgH0p^5ftf%}IW-;Jh5nL1P8kH6nR zwPb3pr>LQSh7ztPzL>eXB5N;G!jvnEG(ZjO+A1cczU)ku>`VNRx^-$$hilnh z+I1OnpP}pijrN`Uh_lN}2RDXd(UAO9kmv)P{@zF~IKnFK@s(^s=D4w(j%hsbP{*Qd z)0J8w1zwYIqT0`j)|&o#{c}*i;_g4U`p6&mE?}7A@jj~oO&Ku{0$vM~TC{`kjn~8w z7n{s4CE#-$((!yPGN%dtkSo=TC*htiRsDMRgJlIwO(21;KA2w{5;un9-ps#wAfjH73Kh2wD zWY91UZaNBbD3_z-LCcE=r;S<*F^{75R;@3e=G1^~>*%jwBMOMH1%B4U-fnN-`eU52 z7@8Sva~>5b9jN?t@0Uhm;LcSVZ=(=ZA3RH!ML^>3oR3Wk7iH#ts#xa*Ns_;LntJH{ zVvk$S<agA@C1GSjbEECgqa;%jzPw4|{^rVx z>gZ?xl;rm^NO%gTqOqCbZM2nkr6+{+hiw2PRF6DEy8ILWype(JOkO*dat|PBDwX_8 zJ6$^^;V02mPBU?4P&kP`TEzaYwx$Z}$p08{Ibwt4YrJ2I<}-nQO2rpV!93ztm@Ux3 z_T!Pq!n3~}8)J-~(jSeRt?oz!ik^%XA>&nF;Fh*&0(OS&b}J5$35OMYzTOl(GUhoA z7M&*!W&20{UAbnsq?6PBBzbmMa_je9f~4eT1t|>bXE8T_gMH^5Xqn}bl$D=xiT5&&fipM;VNONYg(Mk> z=a_BaToU^y>D}`O&mD9|zhQ;g3VB2djN%EZ8%-DyI2qvq}bnR;aE7Kd@qXegEYzsGHN5nzZiDBSxu*R$2#p2X@4E-d42w2M1}FRM7? z<6y0hHqR!DpBS8Yb4AoTR?ycJYzY|@ZVHN`8|f}|vOb#R7wxwcHp?O2wt5iwnl5wa z($hUW&HeK{wt3B=sb9=JW)>^04NFh!WOzj58V~3>zsz9YMY@gpvUiOk2gmzqU2#pC z>c{(Di8?=nzM7Nl9Se~ilhzYQ;M)hRfANN642CK-j!4p2`BWop)bAzm$L=^dYf)1nVs5aWH$U_Tiw_56d}K92*72A^m`T43zDieG}% z)p!Fdb5Nf1+qU+Q!}z!SpIEGbftC90XV;E6#gD#ySiDH~?_R*F{2kNsMf0XZ;g#LK zZ%;bIk?}>TIs98Lr?F)x1jgO5bjkT5TIJ4U?&9sO#ibh0E!{h=19Ue@J^R{Tq?0D} zFRr~>@R*1I;nz|gb;pVQc;D4S7@cS(gioQ-o#aMn`}@weHFm*oqo)~B6Yq$imjc3- zK`P1>CcF}D>Xty8>Xxt`t=yGPJKA-$^eLgD&uS!s&{@(wf81}{`3-9&4632C2ze$u zOEObJXsg-+lD@0c;*Fk7THaHvx}F4G`hqt>v`Q~1ZKXdkQ>(0GEp`clZNROGlG9KA zV)txFKR2;)cJLcjfxYCelu~003ql8>eypwQ7X=V@i|kA=6l&%R-)G%wW-ssEr8gky zb_doKPj-U);)K?ABN@}2Gcb;kAtm&pX=3#9Do?jE>`@ybxw1|pR2wTV4>50}H~%!> zz_UE&*NC+JmY_@9bWid&G>04QG5gzZNG$OE4U$*BUQlKs@@1WSH(w{#-2@z0Q;dd4 zD9XB|BpFihNk4@0Ii(FbUvS4FXJus>{z6k{AgFiBLc)dH&%SHaQH3lhG6_r2cz7HC z0D*4WRJIld<2X+$g#QfUg^|*Q$awU>d*F&Hnm+2bzU>Iw`oP<+6x|y(k2I7f1q0i` z(x{j;I*x~*JOW>~H_4znt!&Oc$$&xIO=M3&(<-aXm>!Ft3Or2?C7jgd$S;nr9JA{~ ze(}0$RUEeN#zW#-S+TS9GWz|N_BXFdQ9&I20tsQ zrPVBodp5cP$5hNM?WO8`p^3KavNk`;=>K$NH;+6wk9I2sr^fgX-=#-+Y+}FTP5<<+ z@{wcW(qWb+a81c;lSYA1t=KMS_e4yz; z^=h3W19tr;0Fk+^32e2L%)l|C>Du#Y&0Th1qHic+OY0z~>gf94Kt{DoO^=7JBv~lqr#` z{yR~4m#;7p{h4DwDegJ+K23%eIY*1fHY;@x2(~SIMZ4 z+aDifTkM{T_^Yag3Ea1rBM?8#x7Z0$!lxOzcIWDr7AN3Nxx1mQBA)OdTtvE zULf2J^9Z#pCQYS70e*m^u&g#aBJWOV_;0G^^AUf$Z=haF!<|1UC2YE_i-Kz5^<=;n zdGDch_0~&=mYkVG->0XlooL!ltk_3zdaK-S1j}>klGob7iw}rq*UY!mHd0Lx*{WJ z+oV+_*9aC}P9F6G)j(@>Iqznw{i>!9i_rR%KyKfB`IFPsrRJ*`(;0lM`TSVzOPvxD z-*@{{uU+5uGbCXHe(PYm=+42G^xZ7HZ}Wo^iU*Y}!U?f62heu0a#gl5luLw17G_d(8= zXkYo$r4cO0DR)adzZFF_TZzGWRABWA%tp`l{darv=H@FT zm4Do5R--WZNFane*DN0KK2I*4M{0f_@_jEqfn(ls8?+q?q57isuTg=PgaEd}5IYg7L2mZc%SsWvCGuL*7f-=tZ#Wa~c46jHpicxxUx zad698y9S%KO|>hN*a%js_PzdF>TvEQX#WL$4}|@uV44dqe#-GV+Jo1D*QL75R+l^)=WbcIo?h6fikbG5 zDE=;N(3Or4U*)p|Ql&A1?5Icyvu-8(wW~}O%F!52e7E-`FHuYNS(1|5#@<#S2Ms_$ zSvQ+n4N|41U>EmC7m0OaG8*!ygNj(it0%)3(Ln`JrY}|ss{$>OnP1eQ@DfSbD+p%zZ!X{Jw8I2#ZHEXCCJ>eiM8DUU1~-*ydp0ZUjwr@YTpr zv+bZAc@JEM&o9FV>T^p)>C%pn4`}!Ktx=SF6pmfN=3mpDXDbVy5MtDpDS^V=_jkUo zx?T35i7aBnJ~%jHwIr1-#-FoV+G%_{SowzkhL2l0WaG z5wxWVao9IN{A{&OWr`uy-0nMJ99&-3Y7^fZwNW)q|7Zt#gJ*SYGA^_x%03YrcGE!RS7tf2bwrSNO0|H1bPO$?B-7~q1yda$943tt7M3bYQGEXM4pTW} z#ktRyV0ZyTjw^BZc&aehl(zttqow<@kATPEsDtje63NKwtm!5(9R4|L_GQ>n;_oy(fGN4 zbtwP9(G84~vQ6qA`dk-_jn64CFWrqQz0Q~e>u2I_7;OD1jmp7<^^eYwq~Joy)80ir{8hIkdC;cu*(id zVCn$ZQMe|ngo2SZxw%FqX@LH3x#gehOZ8u8KP2b4%vcN&+p3=s{|zrwAJYUix~DhP z5~%%T4+KSbjxIYMG1|W*PX6mJ4f6PA{RuCB7)of&J3#N7=x6kBhU`=@%99qJZ!nG^ z>i}s>py|Scs0xbquRJ)tz+-8Q1@QwnYN>8K)3lu6NTDP7qxDA})8Bygmxof**h5mK*{VLAh>Hu0)bPK&K8XhWe z>y5F6+i6lP`o!!s6rgt3wE6n=f{v;7Gm&ex%hm`J4BQw)*i{K`udgECs8=7HuDfMVT1wr9d+3fV2Unk(n1I1Ts_ANUpw=-Ou zVQLDFtrCGGy$<+0rU4@C>-|Y7FV1xA0e7s;CW`h2n`7!?eU40g5puH4oU*3sZB6fcx*2vKf(8nM9lLtDD%MRS$9*Zs! zqn*&q#Uz-oCB~?bDb|&?tn5{onj`1#&crI!4g>Xwp(}f)`vXCE`NeAjdZkRy`M0YO zZU6WZez-XctuYWyg$0}$CiODaeG~M%@iAY~&%|}tkD7RAKd_(!a#pUFS__c) z1$HWilL$OsK`V#qg8#o2La^Q=wuje>mQ#t_Szw3R8dgP~H1akEy#t9}nhhZoz)*K4 z@mDtZe|-J+*}%PWuES6rVktL@MnjOszfCfWkumiZaFL!at@M6sB$-2!2&=k?%%!V3 zw@JIAe{192=N4T)uXKth;txgcxU@R6;7-*>)ksawS^bYT=h#3|=~lPl2=tc=%o{?a zDFhnlZ@*Rf9rq;k4_flzTK=*sc%N;w75ku!2s=?EkNSX-i^>)`K<(JCH2w}hycY2r z`wFAxl4^f$_!W@FI{XHsN$nojeEmLkkL_HrS*yCu1;73D83Vir z7PUc1XO)z6{Ginx-{0D;*FX4Q98q`MTWigc#Z>HFO0*|9y^6x=68e15(ydUudwmof z3f1^GIC+3GbR>4Gf^1{;T#;y0Fc&=Q1E5t>U=kwpkxze;W7*+4N{>N*s(4Cld6`>Xrp-~OV{Ho!MDr1V&zLox{`jPvJSM4Uy4;Z4N*v>Q*=HQ3A; zhL#GOR{FMidk6dB0n5C|-u2Pz0VbEdat2mMGl>(`w0W7W71=S3i)TBlv(=k8Fo!Y) zqNPVW@$hjuczAsco1(kM*N#M3TLQjgz{uOqSNLak!59RZ6@HIAW}^ICtRjDfqiN%1uhxUia}^W?O5Ke7pEPXELn2tW8}pD`B^C z`u0b8j)R}v#%a8}{oqEK?$(k};E&(zTX#0RY;orTEi|& z)KAlhdP71P#le2SBgK;JYvB*@x4kAi*=70;EOpSl(Q9m_fl4^v^__YateP6%*#-V$ z9Yr512`gMvWGqL2$wGfH0SD(I9%IT`+l6}(G`hpOE5Y)*#IIpbe%G6KQaND}+J>aUDeW0XQn2dB%J++E z@3Ja4_T=ZQ3#b%ZDz126s5)XT4NxD>*-ZSsbREAR!pl4PAszpq+7Ns6_E@_L;0rHB z7rAcF&2L>NAkaBZ#JDm1U8A>`vT8{S&3s=;!2%a49L-_39G_Tr4hN(408FE?(I5P*dUL)(<1YCUY%9UO5R$AGeye0Rv{kEL%aO$`|p=dpN zzQ234>yk__Z!T~6I{&nA6!`)=wa6V^u9S^Pry${bUsR z5JUw>U?y+Eq|IUcc58~?3bw8RE&Ha-m8p?{Jb!Zoyrf4mUMd2nE;7&xSc_Za-miV^ zi+MLWiyxA=%n2@iW%j5uJpZIsIDa(S?8z+EL>JoRqG0J!V8xUZc#w|k8lz3E{)5lD z&LukVrZr4&^y`{861<$TemqMDHHVybck_rsTEh6gereHz_MYLRUB%C%!^g9=wmDnN zs^j>W?G?Lg3h&(z6}zd+8-cO=rgB3Lmx4AzvSa;$@uhP!&P!C&o!1Ab>USZNX++(QK zM8v zT=7@;&N~*iXyIcf2QQ^9?VoqFeKpo&zRh3Smu0%{}WF$<4`iZe@ zVkCTAm`RO%-0Z#Rl<^~3?~>pv5As>7Zg$=il6{Y=rykPfI8IOxW$#Dr^f)*Kht6Eu z?-Z)&-AJt(BYHl**xspbCUjbTyT$kNjzy?K)XxSC{_0 z5u(0Ri(t}%u~RmfR}QU6&P>BM1~y^bl63zb2WRnb#@$?HcKc-d=t}gft?PJK31~zI&d+%^$==oE`cLfzxud1CYr>HqGDtJ* zQk^pgIAY;3cj0UWKKkJf+Qp0{t@whRXdsRplOaE@&*VJxseFXth$SKj?h(`6rg2Wq ztWIa3?^zKJr8(j{?tfyg>2Qed9_+YaUV!ZPA?CaI3j6QO z-@%1pFU`9lF_qcD*>3CY6AD%6HVx1&BK<7-=*NB7!6xkB zB+hYx5e{shL!$h`7RYXxY)u*m@@E6alzG%*)VsKB z3<2hLM8+}2KJ)n8z3YP9?TneA$>YVU48$*h9X5TFdq57CSLlB~2PnyvTm>tKj`sVxk!q4gwWW z1PHdtmxFr8a+ZMchsJ;+P{!s@7W-f7Ls&k>&qr9GwxOG31eAV6+;1E+8$VH!gc5H;YlkI&fT)2jQf=a4L}>MshVirP`Ui)6d)a$;pT8aqI>Qo!|l3ic4Og2Y}s z%uou)WqnB6G9dM5^d1dcTvkLt$!_2b_d_jyL_qNN=xJ%7{coqbOd)S<{f+Sqyx}>i zvq->7xPhYkpx1hdaHC6r5ThOkJz7r}K~L0>!M$hW@giU?S-cL+dWiG4O3u2M0tcJ_ zRVWW~1dzAmEyUS)6Ta1D@3LUpErlNRqO+%rxSt5)EL{T9OhT?TSYSrls=`vd5~+84Y1P zJch#f|A4mo!buB9LJ(8@uUKO)ECUb-+rz5@Tt&=x*9xNkGJBxhVya^7maqU}bZhrL zu=QTU?y@G?53{u>#M+;YZ*=W+TBSiO8;GHNr(&p)uw}gwq<3j6^bBUs^&e~4NOt(X zS5FTmrFs_F=*ya%%J;yjZ4^90vRRK%qbpFz4;~(1Yi-0FNyEfk9BO_t9>O2War+Lrs zs*H9ee|$nM969c|)MMu@NPnnsh&}TG_oJ5zv<6lNjT&)FXu!bvDfq$Pd1pc-I?R7* z`HC6>G}~u3X{Y79$7)!10Oz@_5bhc8PHI zp?<69@01MmCJu?iT<}q#l3hJ^m`^70`LB09O$wy}sR?_p7Z(cVZ5dG#Ou~AtwcrD` z@mqk*z~VoGM?U2SL|!OyX=9^jk$+7Hs3CHH5FvSW9y5E|ANl?F^VI7UW!XdYof{Sw z+ghpo^u*34AYK5)Yej)-5%SI4oBW~kr5~?FmH#^Qxt4%~SaHYm>>q&T4`D&i42p`` z`Tk%yO6I55bI|>)EPxH*>syK#dLCA51SLS%n-v0ub@8f*gN22trbRJhSTRFM&_58A z*N*LFYAnQ(#FmpjrAjR>d`qP+K5T0xr5@QZ*)MjQgJ#{ZWxjlP@oR2sMDTLy#XBbX zV|c-!I?;X2?Tu;fW{zAg*$L(O%bEV23gi2koLl2 z_d=R-Ll-!2UDg)L&Wa5+UO*1BNFab!#FyAX2!9`~GY+65{ zPCv#vSE1~-)d$AUFfRQ8Q#&VT$OT*C)0ie}-FRKli~*#g`Vu}GZ@B&Dp3wd0+r2gx zveu9i!ZK^|*`nFDM*L?Kv}E6|>AsASSH-mgiVha)OsgzX#h(^V?BPN`v**W0us0;0ui>f>Ne0UnRjALyz_S(Tx;BNs4$hcG}@myJhfIy;zadl z#gRjCmv-r~q(jSgR`5N*Q&OlO2X2G{RkR@B@3aZN+i$FszXrvgt6wV;lq&y%uHyCM zfxQ2xyBHIb#Cs#UYPP(Ba}tcT5mxq)g%6|W9Em&XSw{g!V9VgpCAot$LZBTRNa%Oy znPhGM2VaAx!s`sKm=^Z0V;=m-tv#(Atip+e(A4SDK)QGT58SUZA|lt%zyAI>xdVGf zd~8YX;_j?HE_Frj6^;k6{FOYO_{@-|b9&weX@!jTJ8#@J%hwm9rCxy9r#UPEMhV_w z^M*()^B39@@2ji=f)O6*b}yhDz4PgkM$BLqIE}{;kH+D+58~=h`zF?ds`iu-{TRFQ6tEnSr6Hp3i2+g_`uoH9 zPZ_Iz-xGlrfzTZuNECn!6hR+pexfaEmHD^*ilt@Lt7jxqas3ZU>yq51e}zL4b@D2$ zDHkk(B=7}(%Qpv`jtH6+W-T)gkAZLp!SFr2>|UgaN8>iJSX=(TSE*+0q%ieW3FWP} zj-@%J2`B(5aC1w=1Y|?OYM3wJMRqR$+qiL>h;`BPeBi{Re26`-!Ag zrNgUpE*&RFPfU0sCSVFU-IKVFv(i25(wiX-nU&pfQ0CV4t(&@)v*d#HyykJP_dA?; zVkAga=hhY%;RP6N-K8pG8D&%&v#_m8yRte~a z`GAd~AP%}4@w0nx$Q6B$90#EU)9N}G?fMe5PWC6GQP_f5H#?Uvz&$+kQwhFF6g}n~V ztX{I->!@HqE04IP=&E2IL8{Inu2MIn(f;4#MmNarO+o-`uv#Lpa!BI@yvGCS75O`5 znqiJZVJ3;t1V%IfPGD{u`Qp>vaw{OGE?=ivt0Qi8yS|7ATQ@B{oWd>l@>Z#g_f&T-{02$_7+VXNm7i91P({;b{2>|c z55JA=9e=#DS;JO}QtLIzXXHxW+%>M(aiepBj5#hSi51D`t2P*q;on{HzZvn~QZ_IOdjZO8}7rIEtEmIkPsq4&yV-wYlKxBN_ckfs`;hU&%X%%VTu@oEL z^(l?}$f{E8N@Td&~6I4Jc9Qnbm<19a`s zW6`6i!a)7K3!6`XR4LF`Mm7KMLxIa`o5Rf=^ zdozbfX50l{2?tif!cIPkcqX@M~Qtz@YY4cIYk3HI_vqe~F=Qa#SOQb%~pscFe{U zY~>ir%=_A=;+dPfTaw$yyM-EsKrnD+6v%H3l!}l~g3xF6hs%&&{ytgR5hmu}V}`vz zi30SB{oc=h)`lv#Y-3k@zMp+ArbRuNw2IXmNQ*mOIvJiep&>)8_Sckic=@-8T)T1dj){k6sG*)T;92ceSLG?!l9_+?ukx>K)p24Ob&lVY4x5`x$`)u}8pPu$upeYfvcHR(t1N@5Rd z^a9Ijn-gRS9|L6;B9qk*$`{ORLDij=%WW1PN*r}8GK)O8@H(#zxX)5fs+OZ@`gOGr*vVtT9k=A&7DiwhKnP zTdMP&m9fltcJn8A;hl`xz)5``8UeubNY>8sf3bhr%|-FoOHZD<;L1t{-n}89)M=5A zG3ZY6$S&{6W$eve5NNd61Aj>dmZ@3J^^ye^lJ8`JBxKSc5q?qMetT57bg^6%+E_vN z^*l#!>vC*p3{IEF0I3jFV>gO}o!dVtb^_N+FKXn{F05~2*w8_>hB?;f6H{^C2CRKy z*FF}4cA$g-!9WCLIA^X~#V+Pkg_Ng7@#kgDueDB|!RVUB2N&Z>*v}i{?Q3saqI8ot z-ie6)$BC9-7qU!F-1d^(l6uZ6`z>w5!WXT_@U@!$v|>&Xw7X1 zO@{6&RpWB3awTc-0GZ5t%GyL=b+TC}z(Cq!Wjc`ME}oT(I?2xASWi@y!Im%q5EcsY zPsGGRLJei$aon_i5Lq}aU4FTbl5yjf{@dEZ6i4*Wk5!WdhI6xjU9KS>m9Tg0@TjP+ zQ)hoGl<0p-zUY3=&E@D#%%i30Hq4ZiuHSZh9yWpYB0lazz0fkSfyuhUvlavPR)+^b zXbd<|{X;i(@tEd}>!RW*Uwu#6%q9r0W3tG_pI0w(san24Ob{sXx+gGZ8y`hg-Q~zU z3LcV~^Tn9T;W>9~_u#-C?~^<0+SuAo*YnfRb-?EQ5b%Oy;M+Wezai=ZVRHPx33t0y zOzrYgf}=+QjmlG1`Zi`-{w_`X?!9cCL>vUy8nZ>?>;a-|8MFb5DS4aEW5tUmexXT^ zNQ$}C7fB>>=2e|fny;CU6gr>R1|zV5M+?HrKoA0<$mB>^+Ff~d!Ip@Q(jracgka)~@Z@+Pt*A`Zj@-EYvFv1q$Es~`R7;$QDK2g5 z;)q1d(HLrVbsU0 z3JZK3v@sufWL(^`0Z|<`KH%^6E0bLFNV3d13DY2Bf6bnDY2DGB-9i8sjs@!&*ZjDg z#4U2#{Mmbw3%aKgbWoFlqiMs5Bdm-oWsV4VkzC;?y`1cl;n@U#N>ncz(XqQmR| zi`uLOkBd)y(OVJkvf#oO#?F6_8`Qa;R>ca0!ZV{$N+9po5HWRzN+iQ^n6pf`}rL^^Du zEqLk=UQ24i0qk*j#xdwi10n5LV0w>uUVeSkG(*hu73(URG-T#}uM8W#f~>b4HZo>^ zXz>_Pqq04Y6*7w7cPe)(+T`s%Q&`x%HD1sCR)_f4&n7ushw))j5sfbMyt&*C(573O za|#LqW)Yk|9?G{)xLc@L*TQM+0A3N#mW$!JqmYjUI|vvyZpUW#Y0H*vnyuanKSS{_ zqdX5^!GtvOTSCOn(|Z(fy?<7xnH|Hoek{B%Z)opryQyoV^&zfm2P&U`0Q+HUg#Q!e zs@wX2XwL2>@A_s$2Bbu^`3ghH<>iYmq z$D7DZwD}`f_`C}6T8N2C4rf<SFeM>}viMuUl~w^NnmPA!_T(z$sKQ zkZs1?*LY8<>bJvxY#}q1pK)fnYb{T-->NvR9E@3OKlf2gOzL$8Us<}yq?}|4wfZ~p zl*UHs`RuI=3j=2#u16PRJQg=gI)wJM&9Rwm`4?AyF?fQqhQV z(oNY1Dj(_7V|qgKo1cH*`G?)hM7HfHKF-Dt$QJ%j$7A(EweQC84_gba#=aZT z#k=$961pA>1OiF%fYpdd4~HPP6LZ`_4&M2qA0xrFHl`hpF`uo#s|UPR@wsrz;J|Po z{u`My#`G*)CI+h2QX&-dPCbqr{Oy}sXramTEIkR0v1=aZFYC6$mI&<5IN8UQzdPu8 zp3&jlCmk4Gx@lB2r-mN?y}A&-HgdRxdPFhbSZ91(Ll#qgIU4niI=j0IJc;s8BKkaE zXvG2_K?2tFo)Gk#+Zi9oogEZk7WD9=9VY!!-2i{3U!5te%U3N$ZO1bBCnNMu&H=%K z(Tit_y1V9_FYRrHga%1ta(F`uFPRUuNLL+2J`So<)O|kf_b$Di3OF*j=T}Fvz8Li{ zUOe~X$u6(LP?o%TajNjp&h_Md88u{Cf0kUEa#@TeS2~TL%pHvP<#9{;@bSNk@1r>G zl!hB^3C5Nk;JcQsu9b0RArC#_%^_Q_$v4`025`mmP2oskO`}&VLgA4liAL z%@Bwk>!uFXL=UdIN%1h|ZYaKBh8ozqKfHu3`+^S8y1SNf%v+ms`(V%H2cam9`5kBf zg<(m(6YXEPO`2a?1{)YY4dlKaYlFRGQ`Yb3te%f#KH~Sjp zR?CW}yL36zmRV6VM16@Njhv;TzQ^gVWd*Q{;&lDHb@VPJmZ-72T;_Ch_n0(JiS0d) zJ!08Qj70C1-5;3I)F*+A{9<5E!IiwU_iH$do~;k9k1o$exVYPs{Supe0~UVH+lW$P zA8D?A-e1M>#*lC&jODCGom+4}$F}I>$~Id+!pMDh4D;M=zOPB zu;$WG@I^C)`Sh*wVfrrxOmQXF@iLH200Mc%HR39QCrs7$$U z?;rMHT2`U#JzzW5$t>d8oSANodmMq*^G5_6pJR$DHxuu8&?adxu37@gUVy|1HyiPzb0 zF%qmsMmj=*G|$FYLyF`lQaElOTFc|SeY7_bqJPgg`IriPGfK%N0}i4#+&EfYJ1K`-zU4$oXvEVLy03-r9S}_fu%Xa%m2Zd zp!cKe#Dorq5?@8-_v9dz>>1;uy>B4;$9xUb;Za;lhYs@^j^5e_l0L9rFkp#zo-jx! znw_K_=I%qgQ2p}dt+!_d6auTKhCAbIf7vmt8pv%u3jf#2gmc}Kn@%Tr5TiW7w=^>9 z9>4_}_jR>XEzDzmyUE@`gx_AptJ~5HCN%Zc{prk}IqUE_F8#SrN;a$3&tzyDC&g>7 zZrO+y#XUEm2}jXS>z4QHm`VflfOB2u3ckCd?zK{oTtl7dQ36MLa<3jN41mR6<)%nz zNcUI7vEse_SI9r&T-P-a)Th_QQNy7#+A;yMMH-J6=8S1@3{X#A%T{0{4HK1I6dUy#{1d7Da%p0yi}5)XlOCO_jx?tq46aKKs*zU_G$|#w{lS!OYnK zydqy)_;FGRqcI9EKFqjeXgr#ZxJOI&;6%`U z&s)-?0X5<+csbR)C(Mt(j|>}aG|)>K#iIOh5BzWgKhR|0|DZZ$qaeAdx*O?U%CT9v zP8J%V7w~DAPzuw(n-WaB^zWx7eyT3%z`w8mkN-@B&~T~AHFUVb$JqUDTsAT{%-44c F{vQw;5DowU 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 0000000000000000000000000000000000000000..47f7ad01148581becfa629bc3df518ff1b152d4d GIT binary patch literal 3749 zcmb_f=Q|sW*X~rQQKBtTqaya+qoqVbVvn>)ZB<)KVl**2VwKuNm1^xOHHwN^Gghmy zw<_w1Rilzete@vUct5=7y3V;joN=9V-5<_5vF4@*Y?pa2pE+}e4T*qRo?_mA%FJ|X z+jp+tICF;G2MNtWR(uigusLRy7MpAoAcP(B?hqE|eg^4rH2^f2GP{)ONsWz$e+e61-L9La! z8wlt*x?fNS+i`@FJ74$uoEAR(AcuVPokTco;{Q$HoUBueFLdW$=(f@oBL3~m`u?5f z!=MR!fTA9O?BGy(y_5U|Pdw}Ky*C>YDJgj`;(zvm8AtbDa6&k?ZgIHF5!`!PVmbLrg?OL^P4ShlIgH zmxM%r!EmJH8lLU)7cD%;KVNuD`xSr2Cc%sgx}H|>|LuxNm$DYM0QG~VU8==hCfo{$ z6lG=Qe-zl8*&j$tN*Wd^ZVK>mzH&}Yc5t~Yv+@t$?A(-BBe7st3Cpap?BPlE-mfNY zuPZ?$de4BOa z9#8|$}ho=6}0oO)nA1J1wp&_PpIsHIGb|S0wriDdn zXADc4Sl%H@K6f0WyE?C3$5_CJaK6%yyA9x)h?qpbYxoi`W+s4t+M*(+ChzwIv#Uq& zm<-|U<$jrxw0ONgy|VBF^=D`7VuYKw=Zus|XS*M`FI^rfJ*nkpjHIY2XUk+vn|!ga z>wIvVyKrL>kHcd^ea>eR0R0c|)=g-ftmLZ2rT6 zOa;o^+&xhwES#bV1C&qVQcR2-9OxpM&THcpi7V=xnS5N{-ewyGu3f!<_wMzS#J}Fr z*I)}_jKrG7_Ei#??We_^HM*0s)6-Sj{Qfw0i;Zj7#G7W2t==<YZ z#%ZJ^`Tu~Yk<>$%wid2<5}PvhJB)|iufGy^#ZSA>YR#N1Z(3952Zg72L?6{d_+&F= zLD8e5Iei&i*9J#M!=WRqo-Xtm8I^CBL6_9PjPZWZjZb!@>LmS+NvvJwRInts43nOv z;S|BY(dgs=b{D=cC70o2XCjww)DL6OGnkR$I^+axK{1Vv?A-=mO{})RzV`LZu2+@2y4d8pMuO#&`^1rQy<-Gm^+_d)hZ^ z{b)3rE6hF1K=_F5$k5~3+Nc4Vm*-z--4bY*UR&HBGgk97;mYKW5dmVydmR9hB}$p? z3`{O+ijBOIk+Ii>V!WiOVg^9gGp)G?AD0M2`SX7H7Ot|dcqI%I2=*bj`BNe89RArk zB{d|Nw}7xN^t}VYsb7}|9%``zrM>T?$H?MTDP4({0g(0UoMgoO;4jQa)YO|;4T*At zW(KkHqeGH{G1ySxhv5F^&pUGL|6G$5>evV>?&qMu<=z+##JUz5^^Er-aK91HIpCqD zUamQ}!w>azJ1=~+Y+Q6mO_!$Hf2Kdb366^yGXi$o&8Q9M62g{GgJGeTUal&t*|SC? zjJIG(6qM;zz@2oo8gbadz%fM+&L=Dkq?K_$;6u1R*-V|W$>A^F#}71ENEi~$>%EsE zP>i9p@|P_SWMgZKcEG3OHm{B<82}LuKY~aB1%JX>LflPrvkhGI=R!Q^s}x19<~5)A zehB>dQDaXbtk>iaHsF)cY$K)ttkx{N`VVv6hrZ&fDhk-Wf;=Yk=+gpsO~)HEo*)3# zfWLbuxNyw-#2T+QO#J3#tJe&Ek1QhC?>&zXn5cB_c+5*}A>MY@@LvyO*t=&DwAx;# znP2tK%NCr-_*U^LU+#_1FYZ;nQ$F^1&vGs+#j~&8Nt$wUsA%kUsTz>4zF-+?C?3=w zk&^5}y`OpOPHs!Qna*NAq?iovo6s# zQ$tGcv{qoiDur4(UVQ5F)0m6LC0TCPI8-8UmkHD zTyc}*s>Nl581q9=Q2P2_$Xp8k*wS{gxK&8hs_7g=c_;ue7ESl($2|Oomefv>VFyYQ z8dg`40cN8+2Vr-7W*heI0hKg=^BR`qxfRcxFJR|C{Ot%U%o1C&2691A?83JN25(g- z7B1Eo$^2=~HTX4@Uy)3xzH+^g^5ZR$NXKl?yirG#R3eR@Jb8ka=cw%Wwc!Q%;^~_X zF4q!Pd87esH9l9OzP8BGnxOd$cFu{HPL7gpI}4OdDU6K739e{ytZsMu6=SSDJw2IvSx$P7T&$b2 ze4ALd$?aP7^(e5W)mw>yG-|vcP%@aQIpXB_Lk%UIdB1X9=UoRC>oe1g4^{+pgjp|i zR{yj_v$HR;{@FQyFvnq5%A$VpI}^`m>2mzQfqMn-y(ULog& zUT3JR%SSyEfXvTK)DL>7o)8>A#XcnG>a+_%8;Y81_ef5e?>K2?gKJ@lFG*P&8h+m1 zH+(b6Y8cTLd4WB z_*((2%WSH$O9It6?Pu5sop8N&A!%1Mx{kCL+TX8-Y&VV0r@h5*qD2uUMj}<%fOCN8 zmUxIz8_L?cF@c?S`}80g8~VGKy^q=P48EP3NJ!e!OjVHR-CBNI9V$r9xm)!7tmCL! z`k$=DMQ*NI=LUIESa7JlsnZ1LS>m1Jkna7A-y3brw++>nkjUc2fq7LeM`P5~#Y0Np z;W`gj>e_w%pwef(X_c1=L)ovYfU)zE^*f>0!qM&8p!5(a8YWjAM z5`A*4cYkfa5AlfwH~T50v~MRkB01MlC&o*ABdU1ov&(U1$jmK$?2?+5XE$ZjpOKZ9 zcT1|T6<63?z9*mKhH*}+_|U-U{50-(K(^Z6{tf@j`+0vwX7^lHr-@ll-dX$4UJC^m zG_6Jx7Q|S+js_xGxMyhJGi1!}W)NEg>y8Ij*zpGd!4+RF;#46`d}a0TUAwdu27T756x9xfWMvKfmLpfxq`_C9mN%@3K3KVK^1zuG2ge zX(hD^un-uCOH2PM2j2MW=)+M$PDy&mhswgMM~HC6w(7jeXIp3tFU4jkRzm2793{iQ zreN3o>du$NzrwE$hglGI#sl#E%0D~=uyN1!1!j<_T7iQg* z$%c3oTA!JGQ4f04NMAGq23Qv0W(DZ>C>kabL=ntjVPUY4IU!zpRTuh>6=Xox`45Jx zza?>qKN#rgX97Gs_n>BL=+qn2QxZ+k(N%;(-!X%%u%`if-IV;=Q-pQjmVsR8OHw!r uVuM^T)V&CJX8Dw$gH-bRKYws(Ur)|{H+V3wRkI-YUlI~-3i||gdj3C~ss*nA literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index fe08f96ff2a7e5db10f72f90db9e5aa64b8624ac..865214f73f82599eb8024e40b647bcf3c236e55d 100644 GIT binary patch delta 2756 zcmV;#3On_e70VTnBYz5#Nkl8OKiRb&6ba0j+k5m+EoHwc4F(XV3m{ z?QwIsmd=jjoW&!`HC(j{N4#y)V>jEWleJ!_)-vZPa}~uP7tsI#k|3Z)t0JN(5)=i2 zglEt5eqWOBOTHu^Omoh2jNyBq_xF45@0)me_4x$@yh1X6;D5D3JBa=3=_Ps=*S@e< z8ocC{c6OlDpZwFRNBFI+QOXnbuaeMv`b=66>L6V&5_sSzx=8`ahGohm!)|4gK3|b+ zFe(#ujriWg1mCmI@Ne`CJxgnzKM``hz{<+xswD{MlrmZ0iUP_cUC)Kqu(ef7&`ZOm z_UOkKALsmg+<$bXGFf+73|ilfV(FoDi^-6#s+G2!U>@$T{17B2TIx;QTw@2WwF9<+TI0bv&E%!naz-0Ru2_V#12SS)U(u&N#=$Ez`hwS6XG#UPJ8Uy9IPa>VK!I*H}sg3xiQHg9g$ z!=;8+j|3*EPJuis-z|yh8!p1~gWp5YI=$QK#D!7?C?eER(xJpN3P{rq!TBZ1v_$YQtfv$ zI~gpP3m^|a8Rp;xLD`Jgo|1}vq0HM>3nn+{yf4z(XnF$kH|JxlqhVstJ9vV_hSlvnVz!r3|B#&{6!B$7cLYAnxC+gW zI|bG^L(0JmAYYYht2XlJ0=6*`l79rBU7fcnd1E8N zb!K@?;k8k7SB6#z4!TafXT&)*W8&_dtx4gJi&Pm6f z()?89g+G6503{J1gJ2)Q#N|ow1XSl6y@S)n+X%>(9-V*>G`l0PKD!>wf`3gK9<;)| z)La%i&kCMoa;m{8H20mUg@C9$dkH4Jl>9saWn78L_x){C>>>2KeL z`CoM`?yZ~LZ)t;HZY|`k)PH-+TV>WCtKnvocr4j*q6(BqK;fu2uGE4!(OAXzM!lCb z!A5`*LJ{XaiI?uX1`h?vM(sa#ua*&bR`9Gi{9Rley3k0U94r9^hNiRLGH#rR%)2&X z#v++;oEt1xojd1_z*}2sfDhf3g-0vcSnc=m@k5At5Qm)`B?kB}I~FClppqOXB9B}b@ZXhQ2W9wwt>sc2j)^)tR(9jK13tgmp1|fH(Xo<0o(&C{K2{!6e8tm| za%!p`j$F7aNq>U6wF%4O4Xo{#?zBNI%Juv0(A3(AKrNEsb1UlL^`soG7)TWY{zxfX zb=o*YF=f2;!JcC@#CP$YwcHr%&I>wtEma5m3mcrmsW(DllT&tsfoEM;vWn?#*7pwn zd_7^5z8|tPQpzLLT2Dty&NAf?aj+H){5haE^8K9_Sbu*+%Y_Xkt&1vV#UNXrU4huf zxW8uPm<{%RCpj4BjSeyZVP?n3m~$PY=0=4OHm?;KgL-nc=GgL6+=9U<6MQ~8aD=J=Q%p%kb=i|A*a633o_9C!p-!foU7eJo<-a1!Ph=Rlj{6KR zZXuBv?;HG=bd1}l{4H6xCn7|cffj);k!(^taHdNUUG&sv;kHj{&%S{dh10Z`DGrCv zUVrwH|MDy?=MBA`2+I%Wo?#>5JXLnnY-0mXSLpbxe^{1w_^hR&T+HL1eg2Y9^fhZ5 zk)WG^sXhzSH_BG#Tw!-SoJXwU{FN%s@a!bT9Y*L23yW>U@8AgWF^%UFe}q9v1YA1wIS5r22&(I4TSKRzoujPPTUx(S=PU^j@|q*C-3i z($F*XEUm${Q+ORiIh|=QK*$1!w-2G&VM$DJwqocR$clY=l=sKB)PQ>%ZnHQ0000< KMNUMnLSTZlH delta 2704 zcmV;B3UBqx6_*u|BYyw{b3#c}2nYxWd735Xz4i!owke4xcTwzLIgTBH;$QIV03Q-8)koYwjW)2T?ORIOT^ zqSabwP*89x4v$a}EEWb3!9pR4kyi-G=Dn}Gd++HVyBo64yFs+SnKQFz&+Gd=zu!6M zcYfyzuAB%!O(ak40;kn`jUycNNBz52_X85pha`EW2K_3cFlD~ed&}Y(I12Al6iXe7 zWrDESD-c2*T7OYS!yPgjZY2^priTX_gDpE6{kvZ31|op*O%f1*Qr_@>9nO-6h0Qfr zD3+pO_aY5V_xpCqzV>w;Z{Gg_5F$ooSeh9w0mZvybG_pBt`v6H9hcEQqsUO-NzK>1 zs_XBI*8@R@p^GvEbAhh z21NPNt$#mO+}>AGx5IBz6z7P^j`6pR4jkFo2xyt=WCAC=OE%x47FTXUDAv?Mk6ln; z6*w%JlWu@kQ7H8|{vGwQp;zFVrT z3V+o64qkipjScp)n$bfs4*n>Aa8qsEJ;*6-ehVKSH6tKMOj1k}$s}Ahg*P7@%Zv#g zW?f&ws|!l83f+`|u(?JQ%=pD4fFqTFBHX1*$>Wrrb+wJ9cU74a5Q(lNM8jajHV`4; zjR&fkUgI9Def!i(eto9{q`4++6;tK_g?|a`3P9<@<0+BD3X#PrfWQxdADe4M*(Q9WQCBp|MvHv>g2HY2cfMj^$nJd(K<#b zwKlly1EBya7HgdufjcJHaXFIDfmc5GlrKX?Tz6f`5wX%om1xrGJsDk zHvv*Iputm{&!vrXUtI-A10y_;m4Afl@s0yZ09650p<2hA5tuc}la$vD$=b$Ngv9|u zaG~Ge&efkW?THUL)$fdJK}iNLlsK`NO>I9JqOCi5J)#-6m zPOBOufNC3MMqug~S5p3PQ#)M|$EDpy2sA^bn~?}x{HzfoGx+#_hJ-CgyML1Uttu;| z(4xia89Fix32W7~8bDRtw?60sVd^847u%Ea$4~oE9EF46Gu!+nz^UL9($bY~CS!_i zgZ)Y=kyLQi(K|Npe!>V8zv`sfTDtQkG^X_y97SbFEZ&w zm(frdK=C)e0J@BZ zTFnUTIMJV!w_8=_*Nz~c-6osVRy&zenY_DgJ=ltH6z1$#8quKk<$<$+t^hibz%erd z+fHg2DVDb42a_qZ>X!vx5%AQ#yt_a7 zl=VA>7UPJdorP|5t-5tptDae4of zW}g3N7sBq&3El|xc6Pt_#Baek59oFsFp?)mEBSHI<{So!q}>FP)Z35_^8uwWMny-`{?^P`h|y zE8vIZ3w%iHYFU-fprMH5;dgwj+0umh5tv;z?`!7!&*TXn2z~LdZQ8{X2Z`A~kgNd# zIK9gnx2x{5`g}o5DCp)_v#a@T?NxaLKXtB~XEq$>AAiRqD6Wz5(Q|{Bp*~;dU*|83 z_OmAoR7XU&l#D%WDVw-ZD5@>3mkiPsEnw5ZPIewTMORNhWrYq(JdU(_oxMRe z>^;RZ8;|hxdo7&u7n?5FOX#KS|Ku;vhnshAA{OW!)O)7exvgaW+9i&0^)F^A&nQxh zMdPHdVSkrF$~{)f+!lHR5!(79_#+C!QD}NdWs-37M_al!KK2CAL`rBe6F>ntOBb*I zqqTf;L%sxJBm%MM!;ni(npZhY@KImOv5walJW4cpm?5TR}hrYL$J}HAe2jk$g##qc=UnHOfk8jzpSZw>%lBznzme}|5ArZSty?V zdwV~6=?&d?@-WcJ*Ywbx+D0K3+ADO|Eq|)FRZpL-mW-`cJ!Mx3tJ9OYnM6jlpfS*K zKH_UWuAM&cvH!puJApGmAK%cY2C1SDixS(25gK8s{8nYbm>VZxa}^22Qea5KKRMQZNjS^2o9tkkw>56`C^FL{UwcF!IwUYn<00{s| KMNUMnLSTY~>L%p? 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 0000000000000000000000000000000000000000..5b2b4a4f4fe7920e3cfd80c79a6cd2a557617aa9 GIT binary patch literal 2239 zcma)8c{mdeA4j!@7WI?sN9EX>7fq5}Qy7{JIm(uEGFd1gt6Wcoh1|^6kQh^zGFM}+ zIm-Opxt1$(AGt%0U+@3#AMf*gj{iT;_xpT4(buiaK_apu0s;acj5BL&0YWSe4y|5N!N|2|XZmsTg$TaOf&`A97{{yx!5}2#d)jS(mMAVaa?%Xl) zxx5gxL~Em6_AZvng|sGvxN_}4owd*gJ!6){0L>=*!v|}z&~`J$1;lW%yD^Fs0wd!{WZ8JFPcP;0BATqcZKgaa?H4Kau8lO@bsB1jWQI*U-p=mrG~s1ql47< zvqrL&b}ydz$m~A`Xz0(2zu-JuyRG z*>^g=!&L!2N6*GjPCaR<*F1!!k#s1*nFfekis`@AR)4Id% zOAXS5c;0jL#~`>e)6wvv^pfRDA|bKBByBQk;!|7d*ysF?e=<;wq=pF>Tk|BXnJ84M z%8Yo@TTi52CJgIB8-IZp#=P&lac zWJ|a(6m{8v)!10lrX34m(&<0ITn(xVE8;h@rT4L??RY>S6)kcL8aj5>6jv8$ci5W@ z0{{S;vq|Gw{ldAl8L;;=DC$ssjRr6Efoj_z{~cXeAG5M}6!E4lhO8#&5E|_o^mAkn zz{IBPe(_HTgq+VyOG^WHyV&=g*58a)R?}nAFQrwdKzyd#Vg2HnI7L(PEsm7RUYa<` zn=&C5p4qb=FtL=_ZFz#{zbLv!;EseJ41*Ery7gtA+ewpF;cc;4*_@0Tc*1Y63!Ulw?1u?@!5Uw(d??hhcBM;i z&hJKVtY1b@1U}DXKBqdx@#9!DW9<5#7a7cDEy%voP;K46rzOis@R$Y$yYu0t~WZ z3i%|-s4w{{%WY#^RP>6|@m$r|#K23uRcU;Gk+b7ryW4fi+U=hD!AnEzkKRYVpL}af zR-vr2*QUd_Uwug}$tx4ZgCJeqqi5{RV}4`E!(H9on(%?p;~BY6H~&)_@3vnbH(#Be zKRvZ?`Q#vy6=Lh;lp#c${b^F5Fu52M4FCLK-c3H^9Ov`}Dmo@VKVO-0_4Cty107Ep zv@d?S1U3-Mg|9O)Hp^lZk&>pj;`nU_hZ7?7dGM9u*0a77bJG@C1B*@v&0KDJV>(06 zS%fFz4*ym~yiabsyH|UWSGP0ZXm7u+iL@OBLo#e`E_ZAD3}pYDpVt>x$1c~RRUL{# zZpvYyss@B7d+4;5Ktg6B`;3u8d~B_A0p&t)Pz}bG)Z<3~0rBOR&hD3&liywIgN(LJ z7Kt`F8e+j;7g1ej0n!ak(m6L72X`i#O+D8ppBr>ID$3Yo$}e6alb4>P;B2QXBHtO$ zUW!^$GSXC7&Dh=DCA}*w9KA?uGsj)&JE5~VeJ{|dddDO*bh!MPl#9kms!zNGE9C*D zUixu_ttO?05{)?j8X`e6toHjp;aG7`eW2q0=(j(QW!kA7R#h9aC*|tnS{5?6CMpv} zMZH)!tphXHbJ(Q_{>8RIK*4{*E~U9_=?L|mJ%qcOW2`Te!%@p=qp01~QR&f)s1zy| zxt&@Y!U55@x2_qht!h2G`Qt9Pp#CaWDZ;%#?QYUJUZpzy4O7}=%Tos5l&zI|DnR)S z6O$Unaz8G^i-9-iFe$f3FsY}4Um9VH&4qZz|Fi7PVBtb$g_TdwjKF073=;vQsTHEg H*zMWB=@Cr| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d5f7de42ff0fdced40974e978a750118ed26566 GIT binary patch literal 6376 zcmVP)cbopvlDr#SK&hRIvAd-gC~!49n~vh}AsL`zSE;edqk%^ZwrDyyqK<dsVy;{tPQ#4|rK%nxk8Q z=2HUBHX_-j0M~(8tO`gf5(Ix{qIp(Y*zYTNZv{%B_u7W{#xWQg-yaru<#cTe!rMUv zg!BqP*9Z7_i&lMwg4fU~r~+lA<5+M^e?UcWp5-q^Kyph}pmr6RbZ!ao1R%0npNa># zc?#k8{9UIEZ7z-t$5@IgUsz|Y2Yi$1S}Ada3y5R9uLGXZMb&_ASQ(|Lij0emF?j)w zxuUte7^@ybJt1_QrWry&zbpV>AvaeK;R~X~A}+fJXg)8Q7M)Xdw+10oMY@@htW*VR zcIW}mX{Oo%U)Cx(J)t7BfOEjP=%S_Sn$YPvEmeTFH;>opq)JxS3nXQ%;VRClqPbQ( zzE@36nq6kq|Nf$ZU&co~6(S~;f8rc6#TwPiF9-u6tAY&jzdG>MMK3P!WvxN2vxWfm zW=Z#;TGc}d+}W~(N*l^3rzA0gKbG)$p%VPn$E9P#ob)kxZY#gpWVJIHSS)o|&poWH zqOcV$+%Nu|$LM1&&&Lw@4P}8bjwDJr%fSERP>xxYM3kWPKOgHn@)zf-Re}%!FwQ^r zX9K^iUr1!@uZt)P=DPsYO7NKg8EO)!PPPg$i4eU5~R}}CC*?H;` z$j2N4H2G?$gh1j8r>a5W9l3{KWv7RmIICajmoQkU?U=*+XaSS9XTZaPr=lrf*Qs=n z4c`GupXh3+1nLh!H6;!dV}gf^V+GuyNXN!IsRFf^DDR`WnK`KD(-UE3*cEtER4k$b zwu9oU6QG)Oy88J8PDKjen9GiRew5knl-Y*~^(4-xtKS0fnW&*@;CYz0|2N9vVkp3s zD?sTTK{z+YW+gP4uA$Z{q9qio{Nq73_UK|7np&*@{z2uG=oUn)Ip#z?hLq5ZC}DM2 zCgc@96~_W5gi{45y(5VwoWX@*F;EElM=R3dWZDB*aUu)a&ryqHQ5Y3zNp29G>pEqV3G}9(a5PSerKqQxwObHf(*K=h$q-N#Q>mo%S zNVo;9r=Pb+m{i1s3}P{sGna$YuomyVkG%(n@%FMgoLtZi}FRh!XIX z6Qih@YYDjWd#}0(SMC-7H4VsxQZ0>q3`uWd@|k4S+2#W->J*?4K2^Xe8lI{q#~K_%-!+#Y zJ-fi@JqjKd!S})GB%F-3O$i(`GS;+)T}Qi^Unk<+ub6zUza??MF=;bf3yGO|P+a^> z1O@B_rM?0v@CL<#*?Xdoxgv{hngy6$KyxQ#muDq?X~

xJ`6qzf^S}mLq%&v% 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 S$kw=js2~+hS#bz^H8i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..66e6f7341e33e470e469282f69a386ee24f45f31 GIT binary patch literal 10255 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 VF9>ftC076d002ovPDHLkV1i;;ps@e| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3d5ef620f9732ba0925c8e2c815f20820549b102 GIT binary patch literal 9902 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 Hu0mjfk{}CP literal 14545 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 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 0000000000000000000000000000000000000000..49476926dbfeadc86a622666d1aa9af482e0d57f GIT binary patch literal 14915 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

#Df5f7#J-#B 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 + + #1976D2 + \ No newline at end of file From 624cc67d9b64684bd4ab399300f0e80396be0183 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 21 Apr 2018 10:17:31 -0500 Subject: [PATCH 07/16] Update translations --- app/src/main/res/layout/about.xml | 64 +++++ app/src/main/res/values-af/strings.xml | 28 ++ app/src/main/res/values-ar/strings.xml | 90 +++--- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-eo/strings.xml | 70 +++++ app/src/main/res/values-es/strings.xml | 34 +-- app/src/main/res/values-eu/strings.xml | 32 +-- app/src/main/res/values-fa/strings.xml | 14 +- app/src/main/res/values-fi/strings.xml | 59 ++++ app/src/main/res/values-fr/strings.xml | 22 +- app/src/main/res/values-in/strings.xml | 312 ++++++++++----------- app/src/main/res/values-it/strings.xml | 30 +- app/src/main/res/values-ja/strings.xml | 5 + app/src/main/res/values-ko/strings.xml | 6 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-no-rNO/strings.xml | 8 +- app/src/main/res/values-pl/strings.xml | 6 +- app/src/main/res/values-ro/strings.xml | 26 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values-sl/strings.xml | 10 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-ta-rIN/strings.xml | 178 ++++++++++++ app/src/main/res/values-tr/strings.xml | 76 ++--- app/src/main/res/values-uk/strings.xml | 4 +- app/src/main/res/values-vi/strings.xml | 154 ++++++++++ 25 files changed, 918 insertions(+), 324 deletions(-) create mode 100644 app/src/main/res/values-ta-rIN/strings.xml 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/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 From 0864f833071a04af44d435d1617d41f495885605 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 21 Apr 2018 10:17:38 -0500 Subject: [PATCH 08/16] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 76c88848b2238edb1226fd44271012766bd5abd3 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 7 Jun 2018 22:03:13 -0500 Subject: [PATCH 09/16] Upgrade to Gradle 4.4 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 52ff6ae93..01c2ed76a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip From 052d26c708e809159a2e7aadf1cc5ef419a0570b Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 7 Jun 2018 22:23:40 -0500 Subject: [PATCH 10/16] Upgrade to Kotlin 1.2.41 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 82209b08a..00b5b964a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ COMPILE_SDK_VERSION = 27 DAGGER_VERSION = 2.9 BUILD_TOOLS_VERSION = 27.0.3 -KOTLIN_VERSION = 1.1.2-4 +KOTLIN_VERSION = 1.2.41 SUPPORT_LIBRARY_VERSION = 27.1.1 org.gradle.parallel=false From 49689317b7fac9995bcbabe90a62d909475d5f67 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 7 Jun 2018 22:35:20 -0500 Subject: [PATCH 11/16] Fix adaptive icons; remove obsolete folder --- app/src/main/res/values-ta-rIN/strings.xml | 178 ------------------ .../res/values/ic_launcher_background.xml | 4 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-hdpi/ic_launcher_foreground.png | Bin .../mipmap-mdpi/ic_launcher_foreground.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../mipmap-xhdpi/ic_launcher_foreground.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin .../src/main/res/values/colors.xml | 2 + 11 files changed, 2 insertions(+), 182 deletions(-) delete mode 100644 app/src/main/res/values-ta-rIN/strings.xml delete mode 100644 app/src/main/res/values/ic_launcher_background.xml rename {app => uhabits-android}/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {app => uhabits-android}/src/main/res/mipmap-hdpi/ic_launcher_foreground.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-mdpi/ic_launcher_foreground.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png (100%) rename {app => uhabits-android}/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png (100%) diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml deleted file mode 100644 index 9e1881972..000000000 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - பழக்க தடப்பாதை - பழக்கங்கள் - அமைப்புகள் - திருத்துக - நீக்கு - காப்பகம் - உயிர்க்க - சேர்க்க - நிறம் மாற்ற - பழக்கம் உருவாக்கப்பட்டது - பழக்கம் நீக்கப்பட்டது - பழக்கம் மீட்கப்பட்டது - மீட்க ஒன்றும் இல்லை - திருத்த எதுவும் இல்லை - பழக்கம் மாற்றப்பட்டது - பழக்கம் திரும்ப பழைய நிலைக்கு மாற்றப்பட்டது - காப்பகப்படுத்தியப் பழக்கம் - பழக்கங்கள் ஆவண காப்பகத்தில் இருந்து நீக்கப் பட்டது - மேற்பார்வை - பழக்கத்தின் வலிமை - வரலாறு - அழி - கேள்வி (இன்று ... செய்தீர்களா?) - மீண்டும் செய்க - காலங்களில் - நாட்கள் - நினைவூட்டல்கள் - நிராகரி - சேமிக்கவும் - சாதனைகள் - நடப்பு பழக்கம் எதுவும் இல்லை - குறிக்க அல்லது குறிப்பை நீக்க அழுத்தி பிடிக்கவும் - வேண்டாம் - பெயர் காலியாக இருக்க கூடாது. - நேர்மறை எண்ணாக இருக்க வேண்டும் (பூஜியத்தை விட அதிகம்). - ஒரு நாளைக்கு அதிகப்பட்சம் ஒரு முறை மீள் நிகழ்வை பெற முடியும் - புதிய பழக்கம் - பழக்கத்தை திருத்த - சரிப்பார்ப்பு குறி - பிறகு - - வருக - இந்த செயலி நல்ல பழக்க வழக்கங்களை துவங்க மற்றும் தொடர உதவுகிறது. - சில புது பழக்கங்களை துவங்கவும்! - தினமும் உங்கள் புதிய பழக்கத்தை முடித்தவுடன் இந்த செயலியில் அதை குறிக்கவும். - மனம் தளராமல் தொடரவும் - தொடர்ச்சியாக செய்யும் பழக்கங்கள் ஒரு முழு நட்சத்திரத்தை பெற்று தரும். - உங்கள் முன்னேற்றத்தை கண்காணிக்கவும் - நாளடைவில் நீங்கள் அடைந்த முன்னேற்றத்தை வரைபடத்தின் மூலம் அறியலாம். - 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/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 2ff3651c2..000000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #1976D2 - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/uhabits-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to uhabits-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/uhabits-android/src/main/res/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to uhabits-android/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/uhabits-android/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to uhabits-android/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/uhabits-android/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to uhabits-android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/uhabits-android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to uhabits-android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/uhabits-android/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to uhabits-android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/uhabits-android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to uhabits-android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/uhabits-android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to uhabits-android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/uhabits-android/src/main/res/values/colors.xml b/uhabits-android/src/main/res/values/colors.xml index 1e65dade9..2bf2e035e 100644 --- a/uhabits-android/src/main/res/values/colors.xml +++ b/uhabits-android/src/main/res/values/colors.xml @@ -68,4 +68,6 @@ @color/black_aa @color/black_aa + + #1976D2 \ No newline at end of file From 88beb7b8835df87e741bfb9baa114d340e418ec5 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 7 Jun 2018 22:54:07 -0500 Subject: [PATCH 12/16] build.gradle: Give higher priority to Google Maven repository --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6692adb1e..a7b2d52cc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { - jcenter() maven { url 'https://maven.google.com' } + jcenter() } dependencies { From b5fda334d44816eb215c43c9c5926a87966e6743 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 7 Jun 2018 22:57:16 -0500 Subject: [PATCH 13/16] build.gradle: Give higher priority to Google Maven repository --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a7b2d52cc..4820f0f80 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,8 @@ buildscript { allprojects { repositories { - jcenter() maven { url 'https://maven.google.com' } maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + jcenter() } } From b0820095f11fbadac9bcbd8ea52cd4a4f09b29a4 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Wed, 13 Jun 2018 21:39:06 -0500 Subject: [PATCH 14/16] Add action for randomizing habit history (dev mode) --- .../habits/show/ShowHabitsMenu.java | 20 ++++++++++++++- .../src/main/res/menu/show_habit.xml | 6 +++++ .../habits/show/ShowHabitMenuBehavior.java | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java index 733db302c..b7560a5d3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java @@ -24,6 +24,7 @@ import android.view.*; import org.isoron.androidbase.activities.*; import org.isoron.uhabits.*; +import org.isoron.uhabits.core.preferences.Preferences; import org.isoron.uhabits.core.ui.screens.habits.show.*; import javax.inject.*; @@ -35,13 +36,26 @@ public class ShowHabitsMenu extends BaseMenu { @NonNull private Lazy behavior; + @NonNull + private final Preferences prefs; @Inject public ShowHabitsMenu(@NonNull BaseActivity activity, - @NonNull Lazy behavior) + @NonNull Lazy behavior, + @NonNull Preferences prefs) { super(activity); this.behavior = behavior; + this.prefs = prefs; + } + + @Override + public void onCreate(@NonNull Menu menu) + { + super.onCreate(menu); + + if (prefs.isDeveloper()) + menu.findItem(R.id.action_randomize).setVisible(true); } @Override @@ -61,6 +75,10 @@ public class ShowHabitsMenu extends BaseMenu behavior.get().onDeleteHabit(); return true; + case R.id.action_randomize: + behavior.get().onRandomize(); + return true; + default: return false; } diff --git a/uhabits-android/src/main/res/menu/show_habit.xml b/uhabits-android/src/main/res/menu/show_habit.xml index e14ce108c..10bac05df 100644 --- a/uhabits-android/src/main/res/menu/show_habit.xml +++ b/uhabits-android/src/main/res/menu/show_habit.xml @@ -37,4 +37,10 @@ android:title="@string/edit" app:showAsAction="ifRoom"/> + + \ No newline at end of file diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java index ad426f528..37400f30e 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuBehavior.java @@ -25,12 +25,16 @@ import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.ui.callbacks.*; +import org.isoron.uhabits.core.utils.*; import java.io.*; import java.util.*; import javax.inject.*; +import static java.lang.Math.*; + + public class ShowHabitMenuBehavior { private HabitList habitList; @@ -95,6 +99,27 @@ public class ShowHabitMenuBehavior }); } + public void onRandomize() + { + Random random = new Random(); + habit.getRepetitions().removeAll(); + double strength = 50; + + for (int i = 0; i < 365 * 5; i++) + { + if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian())); + if (random.nextInt(100) > strength) continue; + + int value = 1; + if (habit.isNumerical()) + value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000; + + habit.getRepetitions().add(new Repetition(DateUtils.getToday().minus(i), value)); + } + + habit.invalidateNewerThan(Timestamp.ZERO); + } + public enum Message { COULD_NOT_EXPORT, HABIT_DELETED From 6ad302b6976997078ec83745e596c697264c0707 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 14 Jun 2018 22:59:31 -0500 Subject: [PATCH 15/16] Show bar chart with monthly totals --- .../habits/show/ShowHabitRootView.java | 6 +- .../activities/habits/show/views/BarCard.java | 16 +- .../uhabits/core/models/CheckmarkList.java | 33 +++ .../uhabits/core/models/Repetition.java | 24 ++- .../uhabits/core/models/RepetitionList.java | 6 +- .../isoron/uhabits/core/models/Timestamp.java | 11 +- .../uhabits/core/test/HabitFixtures.java | 50 ++++- .../org/isoron/uhabits/core/BaseUnitTest.java | 9 +- .../core/models/CheckmarkListTest.java | 37 +++- .../core/models/RepetitionListTest.java | 2 +- .../isoron/uhabits/core/models/ScoreTest.java | 2 +- .../uhabits/core/models/StreakListTest.java | 2 +- .../core/reminders/ReminderSchedulerTest.java | 24 +-- .../uhabits/core/utils/DateUtilsTest.java | 202 +++++++++--------- 14 files changed, 271 insertions(+), 153 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java index b284f6b34..55966f0c3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java @@ -140,11 +140,7 @@ public class ShowHabitRootView extends BaseRootView historyCard.setHabit(habit); streakCard.setHabit(habit); frequencyCard.setHabit(habit); - - if(habit.isNumerical()) - barCard.setHabit(habit); - else - barCard.setVisibility(GONE); + barCard.setHabit(habit); } public interface Controller extends HistoryCard.Controller diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java index 04ab5aea7..19483dc5e 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java @@ -62,7 +62,7 @@ public class BarCard extends HabitCard @Override protected void refreshData() { - if(taskRunner == null) return; + if (taskRunner == null) return; taskRunner.execute(new RefreshTask(getHabit())); } @@ -93,14 +93,17 @@ public class BarCard extends HabitCard { private final Habit habit; - public RefreshTask(Habit habit) {this.habit = habit;} + public RefreshTask(Habit habit) + { + this.habit = habit; + } @Override public void doInBackground() { Timestamp today = DateUtils.getToday(); - List checkmarks = - habit.getCheckmarks().getByInterval(Timestamp.ZERO, today); + List checkmarks = habit.getCheckmarks().groupBy( + DateUtils.TruncateField.MONTH); chart.setCheckmarks(checkmarks); } @@ -110,7 +113,10 @@ public class BarCard extends HabitCard int color = PaletteUtils.getColor(getContext(), habit.getColor()); title.setTextColor(color); chart.setColor(color); - chart.setTarget(habit.getTargetValue()); + if(habit.isNumerical()) + chart.setTarget(habit.getTargetValue()); + else + chart.setTarget(0); } } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java index 7defa5387..bc9ca2494 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java @@ -404,4 +404,37 @@ public abstract class CheckmarkList .toString(); } } + + @NonNull + public List groupBy(DateUtils.TruncateField field) + { + Repetition oldest = habit.getRepetitions().getOldest(); + if(oldest == null) return new ArrayList<>(); + List checks = getByInterval(oldest.getTimestamp(), DateUtils.getToday()); + + int count = 0; + Timestamp truncatedTimestamps[] = new Timestamp[checks.size()]; + int values[] = new int[checks.size()]; + + for (Checkmark rep : checks) + { + Timestamp tt = rep.getTimestamp().truncate(field); + if (count == 0 || !truncatedTimestamps[count - 1].equals(tt)) + truncatedTimestamps[count++] = tt; + + if(habit.isNumerical()) + values[count - 1] += rep.getValue(); + else if(rep.getValue() == Checkmark.CHECKED_EXPLICITLY) + values[count - 1] += 1000; + } + + ArrayList groupedCheckmarks = new ArrayList<>(); + for (int i = 0; i < count; i++) + { + Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i]); + groupedCheckmarks.add(rep); + } + + return groupedCheckmarks; + } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Repetition.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Repetition.java index ea8951ed4..413c06364 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Repetition.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Repetition.java @@ -20,6 +20,12 @@ package org.isoron.uhabits.core.models; import org.apache.commons.lang3.builder.*; +import org.isoron.uhabits.core.utils.DateFormats; +import org.isoron.uhabits.core.utils.DateUtils; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle; @@ -64,9 +70,9 @@ public final class Repetition Repetition that = (Repetition) o; return new EqualsBuilder() - .append(timestamp, that.timestamp) - .append(value, that.value) - .isEquals(); + .append(timestamp, that.timestamp) + .append(value, that.value) + .isEquals(); } public Timestamp getTimestamp() @@ -83,17 +89,17 @@ public final class Repetition public int hashCode() { return new HashCodeBuilder(17, 37) - .append(timestamp) - .append(value) - .toHashCode(); + .append(timestamp) + .append(value) + .toHashCode(); } @Override public String toString() { return new ToStringBuilder(this, defaultToStringStyle()) - .append("timestamp", timestamp) - .append("value", value) - .toString(); + .append("timestamp", timestamp) + .append("value", value) + .toString(); } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java index e465eed3e..b13e63b61 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java @@ -135,7 +135,7 @@ public abstract class RepetitionList public HashMap getWeekdayFrequency() { List reps = - getByInterval(Timestamp.ZERO, DateUtils.getToday()); + getByInterval(Timestamp.ZERO, DateUtils.getToday()); HashMap map = new HashMap<>(); for (Repetition r : reps) @@ -187,7 +187,7 @@ public abstract class RepetitionList @NonNull public synchronized Repetition toggle(Timestamp timestamp) { - if(habit.isNumerical()) + if (habit.isNumerical()) throw new IllegalStateException("habit must NOT be numerical"); Repetition rep = getByTimestamp(timestamp); @@ -213,7 +213,7 @@ public abstract class RepetitionList public void toggle(Timestamp timestamp, int value) { Repetition rep = getByTimestamp(timestamp); - if(rep != null) remove(rep); + if (rep != null) remove(rep); add(new Repetition(timestamp, value)); habit.invalidateNewerThan(timestamp); } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java index 99fc5051b..e79ef5609 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java @@ -20,6 +20,8 @@ package org.isoron.uhabits.core.models; import org.apache.commons.lang3.builder.*; +import org.isoron.uhabits.core.utils.DateFormats; +import org.isoron.uhabits.core.utils.DateUtils; import java.util.*; @@ -138,13 +140,16 @@ public final class Timestamp @Override public String toString() { - return new ToStringBuilder(this, defaultToStringStyle()) - .append("unixTime", unixTime) - .toString(); + return DateFormats.getCSVDateFormat().format(new Date(unixTime)); } public int getWeekday() { return toCalendar().get(DAY_OF_WEEK) % 7; } + + public Timestamp truncate(DateUtils.TruncateField field) + { + return new Timestamp(DateUtils.truncate(field, unixTime)); + } } diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java index 4e27c9960..929367fdc 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java @@ -26,7 +26,7 @@ import org.isoron.uhabits.core.utils.*; public class HabitFixtures { public boolean NON_DAILY_HABIT_CHECKS[] = { - true, false, false, true, true, true, false, false, true, true + true, false, false, true, true, true, false, false, true, true }; private final ModelFactory modelFactory; @@ -58,9 +58,9 @@ public class HabitFixtures habit.setColor(4); Timestamp today = DateUtils.getToday(); - int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, - 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, - 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; + int marks[] = {0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, + 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, + 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; for (int mark : marks) habit.getRepetitions().toggle(today.minus(mark)); @@ -81,10 +81,10 @@ public class HabitFixtures saveIfSQLite(habit); Timestamp today = DateUtils.getToday(); - int times[] = { 0, 1, 3, 5, 7, 8, 9, 10 }; - int values[] = { 100, 200, 300, 400, 500, 600, 700, 800 }; + int times[] = {0, 1, 3, 5, 7, 8, 9, 10}; + int values[] = {100, 200, 300, 400, 500, 600, 700, 800}; - for(int i = 0; i < times.length; i++) + for (int i = 0; i < times.length; i++) { Timestamp timestamp = today.minus(times[i]); habit.getRepetitions().add(new Repetition(timestamp, values[i])); @@ -93,6 +93,42 @@ public class HabitFixtures return habit; } + public Habit createLongNumericalHabit(Timestamp reference) + { + Habit habit = modelFactory.buildHabit(); + habit.setType(Habit.NUMBER_HABIT); + habit.setName("Walk"); + habit.setDescription("How many steps did you walk today?"); + habit.setUnit("steps"); + habit.setTargetType(Habit.AT_LEAST); + habit.setTargetValue(100); + habit.setColor(1); + saveIfSQLite(habit); + + int times[] = {0, 5, 9, 15, 17, 21, 23, 27, 28, 35, 41, 45, 47, 53, 56, 62, 70, 73, 78, + 83, 86, 94, 101, 106, 113, 114, 120, 126, 130, 133, 141, 143, 148, 151, 157, 164, + 166, 171, 173, 176, 179, 183, 191, 259, 264, 268, 270, 275, 282, 284, 289, 295, + 302, 306, 310, 315, 323, 325, 328, 335, 343, 349, 351, 353, 357, 359, 360, 367, + 372, 376, 380, 385, 393, 400, 404, 412, 415, 418, 422, 425, 433, 437, 444, 449, + 455, 460, 462, 465, 470, 471, 479, 481, 485, 489, 494, 495, 500, 501, 503, 507}; + + int values[] = {230, 306, 148, 281, 134, 285, 104, 158, 325, 236, 303, 210, 118, 124, + 301, 201, 156, 376, 347, 367, 396, 134, 160, 381, 155, 354, 231, 134, 164, 354, + 236, 398, 199, 221, 208, 397, 253, 276, 214, 341, 299, 221, 353, 250, 341, 168, + 374, 205, 182, 217, 297, 321, 104, 237, 294, 110, 136, 229, 102, 271, 250, 294, + 158, 319, 379, 126, 282, 155, 288, 159, 215, 247, 207, 226, 244, 158, 371, 219, + 272, 228, 350, 153, 356, 279, 394, 202, 213, 214, 112, 248, 139, 245, 165, 256, + 370, 187, 208, 231, 341, 312}; + + for (int i = 0; i < times.length; i++) + { + Timestamp timestamp = reference.minus(times[i]); + habit.getRepetitions().add(new Repetition(timestamp, values[i])); + } + + return habit; + } + public Habit createShortHabit() { Habit habit = modelFactory.buildHabit(); diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java index 51c3757ce..2892db0c7 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java @@ -25,6 +25,7 @@ import org.apache.commons.io.*; import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.database.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.models.Timestamp; import org.isoron.uhabits.core.models.memory.*; import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.test.*; @@ -92,20 +93,24 @@ public class BaseUnitTest DateUtils.setFixedLocalTime(null); } - public long timestamp(int year, int month, int day) + public long unixTime(int year, int month, int day) { GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); cal.set(year, month, day, 0, 0, 0); return cal.getTimeInMillis(); } - public long timestamp(int year, int month, int day, int hour, int minute) + public long unixTime(int year, int month, int day, int hour, int minute) { GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); cal.set(year, month, day, hour, minute); return cal.getTimeInMillis(); } + public Timestamp timestamp(int year, int month, int day) { + return new Timestamp(unixTime(year, month, day)); + } + @Test public void nothing() { diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/CheckmarkListTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/CheckmarkListTest.java index bc79140af..66151fd7d 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/CheckmarkListTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/CheckmarkListTest.java @@ -28,9 +28,15 @@ import java.util.*; import nl.jqno.equalsverifier.*; +import static java.util.Calendar.JANUARY; +import static java.util.Calendar.JULY; +import static java.util.Calendar.JUNE; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.core.IsEqual.*; import static org.isoron.uhabits.core.models.Checkmark.*; +import static org.isoron.uhabits.core.utils.DateUtils.TruncateField.MONTH; +import static org.isoron.uhabits.core.utils.DateUtils.TruncateField.QUARTER; +import static org.isoron.uhabits.core.utils.DateUtils.TruncateField.YEAR; public class CheckmarkListTest extends BaseUnitTest { @@ -360,13 +366,12 @@ public class CheckmarkListTest extends BaseUnitTest Timestamp t = Timestamp.ZERO.plus(100); Checkmark checkmark = new Checkmark(t, 2); assertThat(checkmark.toString(), - equalTo("{timestamp: {unixTime: 8640000000}, value: 2}")); + equalTo("{timestamp: 1970-04-11, value: 2}")); CheckmarkList.Interval interval = new CheckmarkList.Interval(t, t.plus(1), t.plus(2)); assertThat(interval.toString(), equalTo( - "{begin: {unixTime: 8640000000}, center: {unixTime: 8726400000}," + - " end: {unixTime: 8812800000}}")); + "{begin: 1970-04-11, center: 1970-04-12, end: 1970-04-13}")); } @Test @@ -376,4 +381,30 @@ public class CheckmarkListTest extends BaseUnitTest EqualsVerifier.forClass(Timestamp.class).verify(); EqualsVerifier.forClass(CheckmarkList.Interval.class).verify(); } + + @Test + public void testGroupBy() throws Exception + { + Habit habit = fixtures.createLongNumericalHabit(timestamp(2014, JUNE, 1)); + CheckmarkList checkmarks = habit.getCheckmarks(); + + List byMonth = checkmarks.groupBy(MONTH); + assertThat(byMonth.size(), equalTo(25)); // from 2013-01-01 to 2015-01-01 + assertThat(byMonth.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); + assertThat(byMonth.get(6), equalTo(new Checkmark(timestamp(2014, JULY, 1), 0))); + assertThat(byMonth.get(12), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 1706))); + assertThat(byMonth.get(18), equalTo(new Checkmark(timestamp(2013, JULY, 1), 1379))); + + List byQuarter = checkmarks.groupBy(QUARTER); + assertThat(byQuarter.size(), equalTo(9)); // from 2013-Q1 to 2015-Q1 + assertThat(byQuarter.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); + assertThat(byQuarter.get(4), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 4964))); + assertThat(byQuarter.get(8), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 4975))); + + List byYear = checkmarks.groupBy(YEAR); + assertThat(byYear.size(), equalTo(3)); // from 2013 to 2015 + assertThat(byYear.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); + assertThat(byYear.get(1), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 8227))); + assertThat(byYear.get(2), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 16172))); + } } diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/RepetitionListTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/RepetitionListTest.java index 06eb153c5..964317486 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/RepetitionListTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/RepetitionListTest.java @@ -189,6 +189,6 @@ public class RepetitionListTest extends BaseUnitTest public void testToString() throws Exception { Repetition rep = new Repetition(Timestamp.ZERO.plus(100), 20); - assertThat(rep.toString(), equalTo("{timestamp: {unixTime: 8640000000}, value: 20}")); + assertThat(rep.toString(), equalTo("{timestamp: 1970-04-11, value: 20}")); } } \ No newline at end of file diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreTest.java index 5a9693312..f5c549a63 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreTest.java @@ -74,6 +74,6 @@ public class ScoreTest extends BaseUnitTest public void testToString() throws Exception { Score score = new Score(Timestamp.ZERO.plus(100), 150.0); - assertThat(score.toString(), equalTo( "{timestamp: {unixTime: 8640000000}, value: 150.0}")); + assertThat(score.toString(), equalTo( "{timestamp: 1970-04-11, value: 150.0}")); } } diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java index 30294a978..d970419c6 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java @@ -124,6 +124,6 @@ public class StreakListTest extends BaseUnitTest Timestamp time = Timestamp.ZERO.plus(100); Streak streak = new Streak(time, time.plus(10)); assertThat(streak.toString(), equalTo( - "{start: {unixTime: 8640000000}, end: {unixTime: 9504000000}}")); + "{start: 1970-04-11, end: 1970-04-21}")); } } \ No newline at end of file diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java index 82e147e16..ea2b97dbf 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.java @@ -57,7 +57,7 @@ public class ReminderSchedulerTest extends BaseUnitTest @Test public void testScheduleAll() { - long now = timestamp(2015, 1, 26, 13, 0); + long now = unixTime(2015, 1, 26, 13, 0); DateUtils.setFixedLocalTime(now); Habit h1 = fixtures.createEmptyHabit(); @@ -72,9 +72,9 @@ public class ReminderSchedulerTest extends BaseUnitTest reminderScheduler.scheduleAll(); - verify(sys).scheduleShowReminder(eq(timestamp(2015, 1, 27, 12, 30)), + verify(sys).scheduleShowReminder(eq(unixTime(2015, 1, 27, 12, 30)), eq(h1), anyLong()); - verify(sys).scheduleShowReminder(eq(timestamp(2015, 1, 26, 22, 30)), + verify(sys).scheduleShowReminder(eq(unixTime(2015, 1, 26, 22, 30)), eq(h2), anyLong()); Mockito.verifyNoMoreInteractions(sys); } @@ -82,8 +82,8 @@ public class ReminderSchedulerTest extends BaseUnitTest @Test public void testSchedule_atSpecificTime() { - long atTime = timestamp(2015, 1, 30, 11, 30); - long expectedCheckmarkTime = timestamp(2015, 1, 30, 0, 0); + long atTime = unixTime(2015, 1, 30, 11, 30); + long expectedCheckmarkTime = unixTime(2015, 1, 30, 0, 0); habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); scheduleAndVerify(atTime, expectedCheckmarkTime, atTime); @@ -92,11 +92,11 @@ public class ReminderSchedulerTest extends BaseUnitTest @Test public void testSchedule_laterToday() { - long now = timestamp(2015, 1, 26, 6, 30); + long now = unixTime(2015, 1, 26, 6, 30); DateUtils.setFixedLocalTime(now); - long expectedCheckmarkTime = timestamp(2015, 1, 26, 0, 0); - long expectedReminderTime = timestamp(2015, 1, 26, 12, 30); + long expectedCheckmarkTime = unixTime(2015, 1, 26, 0, 0); + long expectedReminderTime = unixTime(2015, 1, 26, 12, 30); habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); scheduleAndVerify(null, expectedCheckmarkTime, expectedReminderTime); @@ -105,11 +105,11 @@ public class ReminderSchedulerTest extends BaseUnitTest @Test public void testSchedule_tomorrow() { - long now = timestamp(2015, 1, 26, 13, 0); + long now = unixTime(2015, 1, 26, 13, 0); DateUtils.setFixedLocalTime(now); - long expectedCheckmarkTime = timestamp(2015, 1, 27, 0, 0); - long expectedReminderTime = timestamp(2015, 1, 27, 12, 30); + long expectedCheckmarkTime = unixTime(2015, 1, 27, 0, 0); + long expectedReminderTime = unixTime(2015, 1, 27, 12, 30); habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); scheduleAndVerify(null, expectedCheckmarkTime, expectedReminderTime); @@ -122,7 +122,7 @@ public class ReminderSchedulerTest extends BaseUnitTest Mockito.verifyZeroInteractions(sys); } - public long timestamp(int year, int month, int day, int hour, int minute) + public long unixTime(int year, int month, int day, int hour, int minute) { Calendar cal = DateUtils.getStartOfTodayCalendar(); cal.set(year, month, day, hour, minute); diff --git a/uhabits-core/src/test/java/org/isoron/uhabits/core/utils/DateUtilsTest.java b/uhabits-core/src/test/java/org/isoron/uhabits/core/utils/DateUtilsTest.java index bbf0e5f94..cb6a24640 100644 --- a/uhabits-core/src/test/java/org/isoron/uhabits/core/utils/DateUtilsTest.java +++ b/uhabits-core/src/test/java/org/isoron/uhabits/core/utils/DateUtilsTest.java @@ -45,7 +45,7 @@ public class DateUtilsTest extends BaseUnitTest @Test public void testFormatHeaderDate() { - long timestamp = timestamp(2015, DECEMBER, 31); + long timestamp = unixTime(2015, DECEMBER, 31); GregorianCalendar date = new Timestamp(timestamp).toCalendar(); String formatted = DateUtils.formatHeaderDate(date); assertThat(formatted, equalTo("Thu\n31")); @@ -56,19 +56,19 @@ public class DateUtilsTest extends BaseUnitTest { DateUtils.TruncateField field = DateUtils.TruncateField.WEEK_NUMBER; - long expected = timestamp(2015, Calendar.JANUARY, 11); - long t0 = timestamp(2015, Calendar.JANUARY, 11); - long t1 = timestamp(2015, Calendar.JANUARY, 16); - long t2 = timestamp(2015, Calendar.JANUARY, 17); + long expected = unixTime(2015, Calendar.JANUARY, 11); + long t0 = unixTime(2015, Calendar.JANUARY, 11); + long t1 = unixTime(2015, Calendar.JANUARY, 16); + long t2 = unixTime(2015, Calendar.JANUARY, 17); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); assertThat(DateUtils.truncate(field, t2), equalTo(expected)); - expected = timestamp(2015, Calendar.JANUARY, 18); - t0 = timestamp(2015, Calendar.JANUARY, 18); - t1 = timestamp(2015, Calendar.JANUARY, 19); - t2 = timestamp(2015, Calendar.JANUARY, 24); + expected = unixTime(2015, Calendar.JANUARY, 18); + t0 = unixTime(2015, Calendar.JANUARY, 18); + t1 = unixTime(2015, Calendar.JANUARY, 19); + t2 = unixTime(2015, Calendar.JANUARY, 24); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); @@ -78,10 +78,10 @@ public class DateUtilsTest extends BaseUnitTest @Test public void testTruncate_month() { - long expected = timestamp(2016, Calendar.JUNE, 1); - long t0 = timestamp(2016, Calendar.JUNE, 1); - long t1 = timestamp(2016, Calendar.JUNE, 15); - long t2 = timestamp(2016, Calendar.JUNE, 20); + long expected = unixTime(2016, Calendar.JUNE, 1); + long t0 = unixTime(2016, Calendar.JUNE, 1); + long t1 = unixTime(2016, Calendar.JUNE, 15); + long t2 = unixTime(2016, Calendar.JUNE, 20); DateUtils.TruncateField field = DateUtils.TruncateField.MONTH; @@ -89,10 +89,10 @@ public class DateUtilsTest extends BaseUnitTest assertThat(DateUtils.truncate(field, t1), equalTo(expected)); assertThat(DateUtils.truncate(field, t2), equalTo(expected)); - expected = timestamp(2016, DECEMBER, 1); - t0 = timestamp(2016, DECEMBER, 1); - t1 = timestamp(2016, DECEMBER, 15); - t2 = timestamp(2016, DECEMBER, 31); + expected = unixTime(2016, DECEMBER, 1); + t0 = unixTime(2016, DECEMBER, 1); + t1 = unixTime(2016, DECEMBER, 15); + t2 = unixTime(2016, DECEMBER, 31); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); @@ -104,19 +104,19 @@ public class DateUtilsTest extends BaseUnitTest { DateUtils.TruncateField field = DateUtils.TruncateField.QUARTER; - long expected = timestamp(2016, JANUARY, 1); - long t0 = timestamp(2016, JANUARY, 20); - long t1 = timestamp(2016, FEBRUARY, 15); - long t2 = timestamp(2016, MARCH, 30); + long expected = unixTime(2016, JANUARY, 1); + long t0 = unixTime(2016, JANUARY, 20); + long t1 = unixTime(2016, FEBRUARY, 15); + long t2 = unixTime(2016, MARCH, 30); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); assertThat(DateUtils.truncate(field, t2), equalTo(expected)); - expected = timestamp(2016, APRIL, 1); - t0 = timestamp(2016, APRIL, 1); - t1 = timestamp(2016, MAY, 30); - t2 = timestamp(2016, JUNE, 20); + expected = unixTime(2016, APRIL, 1); + t0 = unixTime(2016, APRIL, 1); + t1 = unixTime(2016, MAY, 30); + t2 = unixTime(2016, JUNE, 20); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); @@ -128,19 +128,19 @@ public class DateUtilsTest extends BaseUnitTest { DateUtils.TruncateField field = DateUtils.TruncateField.YEAR; - long expected = timestamp(2016, JANUARY, 1); - long t0 = timestamp(2016, JANUARY, 1); - long t1 = timestamp(2016, FEBRUARY, 25); - long t2 = timestamp(2016, DECEMBER, 31); + long expected = unixTime(2016, JANUARY, 1); + long t0 = unixTime(2016, JANUARY, 1); + long t1 = unixTime(2016, FEBRUARY, 25); + long t2 = unixTime(2016, DECEMBER, 31); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); assertThat(DateUtils.truncate(field, t2), equalTo(expected)); - expected = timestamp(2017, JANUARY, 1); - t0 = timestamp(2017, JANUARY, 1); - t1 = timestamp(2017, MAY, 30); - t2 = timestamp(2017, DECEMBER, 31); + expected = unixTime(2017, JANUARY, 1); + t0 = unixTime(2017, JANUARY, 1); + t1 = unixTime(2017, MAY, 30); + t2 = unixTime(2017, DECEMBER, 31); assertThat(DateUtils.truncate(field, t0), equalTo(expected)); assertThat(DateUtils.truncate(field, t1), equalTo(expected)); @@ -150,10 +150,10 @@ public class DateUtilsTest extends BaseUnitTest @Test public void testMillisecondsUntilTomorrow() throws Exception { - DateUtils.setFixedLocalTime(timestamp(2017, JANUARY, 1, 2, 59)); + DateUtils.setFixedLocalTime(unixTime(2017, JANUARY, 1, 2, 59)); assertThat(DateUtils.millisecondsUntilTomorrow(), equalTo(60000L)); - DateUtils.setFixedLocalTime(timestamp(2017, JANUARY, 1, 23, 0)); + DateUtils.setFixedLocalTime(unixTime(2017, JANUARY, 1, 23, 0)); assertThat(DateUtils.millisecondsUntilTomorrow(), equalTo(14400000L)); } @@ -162,81 +162,81 @@ public class DateUtilsTest extends BaseUnitTest public void test_applyTimezone() { DateUtils.setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")); - assertEquals(applyTimezone(timestamp(2017, JULY, 30, 18, 0)), (timestamp(2017, JULY, 30, 8, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 0, 0)), (timestamp(2017, SEPTEMBER, 29, 14, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 10, 0)), (timestamp(2017, SEPTEMBER, 30, 0, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 11, 0)), (timestamp(2017, SEPTEMBER, 30, 1, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 12, 0)), (timestamp(2017, SEPTEMBER, 30, 2, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 13, 0)), (timestamp(2017, SEPTEMBER, 30, 3, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 22, 0)), (timestamp(2017, SEPTEMBER, 30, 12, 0))); - assertEquals(applyTimezone(timestamp(2017, SEPTEMBER, 30, 23, 0)), (timestamp(2017, SEPTEMBER, 30, 13, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 0, 0)), (timestamp(2017, SEPTEMBER, 30, 14, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 1, 0)), (timestamp(2017, SEPTEMBER, 30, 15, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 1, 59)), (timestamp(2017, SEPTEMBER, 30, 15, 59))); + assertEquals(applyTimezone(unixTime(2017, JULY, 30, 18, 0)), (unixTime(2017, JULY, 30, 8, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 0, 0)), (unixTime(2017, SEPTEMBER, 29, 14, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 10, 0)), (unixTime(2017, SEPTEMBER, 30, 0, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 11, 0)), (unixTime(2017, SEPTEMBER, 30, 1, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 12, 0)), (unixTime(2017, SEPTEMBER, 30, 2, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 13, 0)), (unixTime(2017, SEPTEMBER, 30, 3, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 22, 0)), (unixTime(2017, SEPTEMBER, 30, 12, 0))); + assertEquals(applyTimezone(unixTime(2017, SEPTEMBER, 30, 23, 0)), (unixTime(2017, SEPTEMBER, 30, 13, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 0, 0)), (unixTime(2017, SEPTEMBER, 30, 14, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 1, 0)), (unixTime(2017, SEPTEMBER, 30, 15, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 1, 59)), (unixTime(2017, SEPTEMBER, 30, 15, 59))); // DST begins - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 3, 0)), (timestamp(2017, SEPTEMBER, 30, 16, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 4, 0)), (timestamp(2017, SEPTEMBER, 30, 17, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 5, 0)), (timestamp(2017, SEPTEMBER, 30, 18, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 11, 0)), (timestamp(2017, OCTOBER, 1, 0, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 12, 0)), (timestamp(2017, OCTOBER, 1, 1, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 13, 0)), (timestamp(2017, OCTOBER, 1, 2, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 14, 0)), (timestamp(2017, OCTOBER, 1, 3, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 15, 0)), (timestamp(2017, OCTOBER, 1, 4, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 1, 19, 0)), (timestamp(2017, OCTOBER, 1, 8, 0))); - assertEquals(applyTimezone(timestamp(2017, OCTOBER, 2, 19, 0)), (timestamp(2017, OCTOBER, 2, 8, 0))); - assertEquals(applyTimezone(timestamp(2017, NOVEMBER, 30, 19, 0)), (timestamp(2017, NOVEMBER, 30, 8, 0))); - assertEquals(applyTimezone(timestamp(2018, MARCH, 31, 0, 0)), (timestamp(2018, MARCH, 30, 13, 0))); - assertEquals(applyTimezone(timestamp(2018, MARCH, 31, 12, 0)), (timestamp(2018, MARCH, 31, 1, 0))); - assertEquals(applyTimezone(timestamp(2018, MARCH, 31, 18, 0)), (timestamp(2018, MARCH, 31, 7, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 0, 0)), (timestamp(2018, MARCH, 31, 13, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 1, 0)), (timestamp(2018, MARCH, 31, 14, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 1, 59)), (timestamp(2018, MARCH, 31, 14, 59))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 3, 0)), (unixTime(2017, SEPTEMBER, 30, 16, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 4, 0)), (unixTime(2017, SEPTEMBER, 30, 17, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 5, 0)), (unixTime(2017, SEPTEMBER, 30, 18, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 11, 0)), (unixTime(2017, OCTOBER, 1, 0, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 12, 0)), (unixTime(2017, OCTOBER, 1, 1, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 13, 0)), (unixTime(2017, OCTOBER, 1, 2, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 14, 0)), (unixTime(2017, OCTOBER, 1, 3, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 15, 0)), (unixTime(2017, OCTOBER, 1, 4, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 1, 19, 0)), (unixTime(2017, OCTOBER, 1, 8, 0))); + assertEquals(applyTimezone(unixTime(2017, OCTOBER, 2, 19, 0)), (unixTime(2017, OCTOBER, 2, 8, 0))); + assertEquals(applyTimezone(unixTime(2017, NOVEMBER, 30, 19, 0)), (unixTime(2017, NOVEMBER, 30, 8, 0))); + assertEquals(applyTimezone(unixTime(2018, MARCH, 31, 0, 0)), (unixTime(2018, MARCH, 30, 13, 0))); + assertEquals(applyTimezone(unixTime(2018, MARCH, 31, 12, 0)), (unixTime(2018, MARCH, 31, 1, 0))); + assertEquals(applyTimezone(unixTime(2018, MARCH, 31, 18, 0)), (unixTime(2018, MARCH, 31, 7, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 0, 0)), (unixTime(2018, MARCH, 31, 13, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 1, 0)), (unixTime(2018, MARCH, 31, 14, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 1, 59)), (unixTime(2018, MARCH, 31, 14, 59))); // DST ends - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 2, 0)), (timestamp(2018, MARCH, 31, 16, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 3, 0)), (timestamp(2018, MARCH, 31, 17, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 4, 0)), (timestamp(2018, MARCH, 31, 18, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 10, 0)), (timestamp(2018, APRIL, 1, 0, 0))); - assertEquals(applyTimezone(timestamp(2018, APRIL, 1, 18, 0)), (timestamp(2018, APRIL, 1, 8, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 2, 0)), (unixTime(2018, MARCH, 31, 16, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 3, 0)), (unixTime(2018, MARCH, 31, 17, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 4, 0)), (unixTime(2018, MARCH, 31, 18, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 10, 0)), (unixTime(2018, APRIL, 1, 0, 0))); + assertEquals(applyTimezone(unixTime(2018, APRIL, 1, 18, 0)), (unixTime(2018, APRIL, 1, 8, 0))); } @Test public void test_removeTimezone() { DateUtils.setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")); - assertEquals(removeTimezone(timestamp(2017, JULY, 30, 8, 0)), (timestamp(2017, JULY, 30, 18, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 29, 14, 0)), (timestamp(2017, SEPTEMBER, 30, 0, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 0, 0)), (timestamp(2017, SEPTEMBER, 30, 10, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 1, 0)), (timestamp(2017, SEPTEMBER, 30, 11, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 2, 0)), (timestamp(2017, SEPTEMBER, 30, 12, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 3, 0)), (timestamp(2017, SEPTEMBER, 30, 13, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 12, 0)), (timestamp(2017, SEPTEMBER, 30, 22, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 13, 0)), (timestamp(2017, SEPTEMBER, 30, 23, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 14, 0)), (timestamp(2017, OCTOBER, 1, 0, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 15, 0)), (timestamp(2017, OCTOBER, 1, 1, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 15, 59)), (timestamp(2017, OCTOBER, 1, 1, 59))); + assertEquals(removeTimezone(unixTime(2017, JULY, 30, 8, 0)), (unixTime(2017, JULY, 30, 18, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 29, 14, 0)), (unixTime(2017, SEPTEMBER, 30, 0, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 0, 0)), (unixTime(2017, SEPTEMBER, 30, 10, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 1, 0)), (unixTime(2017, SEPTEMBER, 30, 11, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 2, 0)), (unixTime(2017, SEPTEMBER, 30, 12, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 3, 0)), (unixTime(2017, SEPTEMBER, 30, 13, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 12, 0)), (unixTime(2017, SEPTEMBER, 30, 22, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 13, 0)), (unixTime(2017, SEPTEMBER, 30, 23, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 14, 0)), (unixTime(2017, OCTOBER, 1, 0, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 15, 0)), (unixTime(2017, OCTOBER, 1, 1, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 15, 59)), (unixTime(2017, OCTOBER, 1, 1, 59))); // DST begins - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 16, 0)), (timestamp(2017, OCTOBER, 1, 3, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 17, 0)), (timestamp(2017, OCTOBER, 1, 4, 0))); - assertEquals(removeTimezone(timestamp(2017, SEPTEMBER, 30, 18, 0)), (timestamp(2017, OCTOBER, 1, 5, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 0, 0)), (timestamp(2017, OCTOBER, 1, 11, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 1, 0)), (timestamp(2017, OCTOBER, 1, 12, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 2, 0)), (timestamp(2017, OCTOBER, 1, 13, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 3, 0)), (timestamp(2017, OCTOBER, 1, 14, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 4, 0)), (timestamp(2017, OCTOBER, 1, 15, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 1, 8, 0)), (timestamp(2017, OCTOBER, 1, 19, 0))); - assertEquals(removeTimezone(timestamp(2017, OCTOBER, 2, 8, 0)), (timestamp(2017, OCTOBER, 2, 19, 0))); - assertEquals(removeTimezone(timestamp(2017, NOVEMBER, 30, 8, 0)), (timestamp(2017, NOVEMBER, 30, 19, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 30, 13, 0)), (timestamp(2018, MARCH, 31, 0, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 1, 0)), (timestamp(2018, MARCH, 31, 12, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 7, 0)), (timestamp(2018, MARCH, 31, 18, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 13, 0)), (timestamp(2018, APRIL, 1, 0, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 14, 0)), (timestamp(2018, APRIL, 1, 1, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 14, 59)), (timestamp(2018, APRIL, 1, 1, 59))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 16, 0)), (unixTime(2017, OCTOBER, 1, 3, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 17, 0)), (unixTime(2017, OCTOBER, 1, 4, 0))); + assertEquals(removeTimezone(unixTime(2017, SEPTEMBER, 30, 18, 0)), (unixTime(2017, OCTOBER, 1, 5, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 0, 0)), (unixTime(2017, OCTOBER, 1, 11, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 1, 0)), (unixTime(2017, OCTOBER, 1, 12, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 2, 0)), (unixTime(2017, OCTOBER, 1, 13, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 3, 0)), (unixTime(2017, OCTOBER, 1, 14, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 4, 0)), (unixTime(2017, OCTOBER, 1, 15, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 1, 8, 0)), (unixTime(2017, OCTOBER, 1, 19, 0))); + assertEquals(removeTimezone(unixTime(2017, OCTOBER, 2, 8, 0)), (unixTime(2017, OCTOBER, 2, 19, 0))); + assertEquals(removeTimezone(unixTime(2017, NOVEMBER, 30, 8, 0)), (unixTime(2017, NOVEMBER, 30, 19, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 30, 13, 0)), (unixTime(2018, MARCH, 31, 0, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 1, 0)), (unixTime(2018, MARCH, 31, 12, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 7, 0)), (unixTime(2018, MARCH, 31, 18, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 13, 0)), (unixTime(2018, APRIL, 1, 0, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 14, 0)), (unixTime(2018, APRIL, 1, 1, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 14, 59)), (unixTime(2018, APRIL, 1, 1, 59))); // DST ends - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 16, 0)), (timestamp(2018, APRIL, 1, 2, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 17, 0)), (timestamp(2018, APRIL, 1, 3, 0))); - assertEquals(removeTimezone(timestamp(2018, MARCH, 31, 18, 0)), (timestamp(2018, APRIL, 1, 4, 0))); - assertEquals(removeTimezone(timestamp(2018, APRIL, 1, 0, 0)), (timestamp(2018, APRIL, 1, 10, 0))); - assertEquals(removeTimezone(timestamp(2018, APRIL, 1, 8, 0)), (timestamp(2018, APRIL, 1, 18, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 16, 0)), (unixTime(2018, APRIL, 1, 2, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 17, 0)), (unixTime(2018, APRIL, 1, 3, 0))); + assertEquals(removeTimezone(unixTime(2018, MARCH, 31, 18, 0)), (unixTime(2018, APRIL, 1, 4, 0))); + assertEquals(removeTimezone(unixTime(2018, APRIL, 1, 0, 0)), (unixTime(2018, APRIL, 1, 10, 0))); + assertEquals(removeTimezone(unixTime(2018, APRIL, 1, 8, 0)), (unixTime(2018, APRIL, 1, 18, 0))); } } From 6ca4877f1f9f9f296d5c7126eb57dc59a7d55dfa Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 15 Jun 2018 21:11:47 -0500 Subject: [PATCH 16/16] BarChart: allow user to pick interval --- .../activities/common/views/BarChart.java | 7 ++- .../activities/habits/show/views/BarCard.java | 57 ++++++++++++++----- .../src/main/res/layout/show_habit_bar.xml | 55 +++++++++++++----- .../src/main/res/values/constants.xml | 7 +++ .../uhabits/core/models/CheckmarkList.java | 10 +++- 5 files changed, 100 insertions(+), 36 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java index b88b44f6f..b27e3281d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BarChart.java @@ -77,7 +77,6 @@ public class BarChart extends ScrollableChart private int primaryColor; - @Deprecated private int bucketSize = 7; private int backgroundColor; @@ -127,7 +126,6 @@ public class BarChart extends ScrollableChart setTarget(0.5); } - @Deprecated public void setBucketSize(int bucketSize) { this.bucketSize = bucketSize; @@ -298,7 +296,6 @@ public class BarChart extends ScrollableChart boolean shouldPrintYear = true; if (yearText.equals(previousYearText)) shouldPrintYear = false; - if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; if (skipYear > 0) { @@ -306,6 +303,8 @@ public class BarChart extends ScrollableChart shouldPrintYear = false; } + if (bucketSize >= 365) shouldPrintYear = true; + if (shouldPrintYear) { previousYearText = yearText; @@ -314,6 +313,8 @@ public class BarChart extends ScrollableChart pText.setTextAlign(Paint.Align.CENTER); canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText); skipYear = 1; + + } if (bucketSize < 365) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java index 19483dc5e..80551e541 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.java @@ -29,15 +29,25 @@ import org.isoron.uhabits.R; import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.utils.*; import java.util.*; import butterknife.*; +import static org.isoron.uhabits.activities.habits.show.views.ScoreCard.getTruncateField; + public class BarCard extends HabitCard { + public static final int[] NUMERICAL_BUCKET_SIZES = {1, 7, 31, 92, 365}; + public static final int[] BOOLEAN_BUCKET_SIZES = {7, 31, 92, 365}; + + @BindView(R.id.numericalSpinner) + Spinner numericalSpinner; + + @BindView(R.id.boolSpinner) + Spinner boolSpinner; + @BindView(R.id.barChart) BarChart chart; @@ -47,6 +57,8 @@ public class BarCard extends HabitCard @Nullable private TaskRunner taskRunner; + private int bucketSize; + public BarCard(Context context) { super(context); @@ -59,6 +71,20 @@ public class BarCard extends HabitCard init(); } + @OnItemSelected(R.id.numericalSpinner) + public void onNumericalItemSelected(int position) + { + bucketSize = NUMERICAL_BUCKET_SIZES[position]; + refreshData(); + } + + @OnItemSelected(R.id.boolSpinner) + public void onBoolItemSelected(int position) + { + bucketSize = BOOLEAN_BUCKET_SIZES[position]; + refreshData(); + } + @Override protected void refreshData() { @@ -71,22 +97,16 @@ public class BarCard extends HabitCard inflate(getContext(), R.layout.show_habit_bar, this); ButterKnife.bind(this); + boolSpinner.setSelection(1); + numericalSpinner.setSelection(2); + bucketSize = 7; + Context appContext = getContext().getApplicationContext(); if (appContext instanceof HabitsApplication) { HabitsApplication app = (HabitsApplication) appContext; taskRunner = app.getComponent().getTaskRunner(); } - - if (isInEditMode()) initEditMode(); - } - - private void initEditMode() - { - int color = PaletteUtils.getAndroidTestColor(1); - title.setTextColor(color); - chart.setColor(color); - chart.populateWithRandomData(); } private class RefreshTask implements Task @@ -101,10 +121,11 @@ public class BarCard extends HabitCard @Override public void doInBackground() { - Timestamp today = DateUtils.getToday(); - List checkmarks = habit.getCheckmarks().groupBy( - DateUtils.TruncateField.MONTH); + List checkmarks; + if (bucketSize == 1) checkmarks = habit.getCheckmarks().getAll(); + else checkmarks = habit.getCheckmarks().groupBy(getTruncateField(bucketSize)); chart.setCheckmarks(checkmarks); + chart.setBucketSize(bucketSize); } @Override @@ -113,10 +134,16 @@ public class BarCard extends HabitCard int color = PaletteUtils.getColor(getContext(), habit.getColor()); title.setTextColor(color); chart.setColor(color); - if(habit.isNumerical()) + if (habit.isNumerical()) + { + boolSpinner.setVisibility(GONE); chart.setTarget(habit.getTargetValue()); + } else + { + numericalSpinner.setVisibility(GONE); chart.setTarget(0); + } } } } diff --git a/uhabits-android/src/main/res/layout/show_habit_bar.xml b/uhabits-android/src/main/res/layout/show_habit_bar.xml index aeaf8f546..410624b46 100644 --- a/uhabits-android/src/main/res/layout/show_habit_bar.xml +++ b/uhabits-android/src/main/res/layout/show_habit_bar.xml @@ -1,5 +1,4 @@ - - - + - - - + android:layout_height="match_parent"> + + + + + + + + + + \ No newline at end of file diff --git a/uhabits-android/src/main/res/values/constants.xml b/uhabits-android/src/main/res/values/constants.xml index 34e8136a5..57c788f75 100644 --- a/uhabits-android/src/main/res/values/constants.xml +++ b/uhabits-android/src/main/res/values/constants.xml @@ -98,6 +98,13 @@ @string/year + + @string/week + @string/month + @string/quarter + @string/year + + 1 7 diff --git a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java index bc9ca2494..9f2c7e5bb 100644 --- a/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java +++ b/uhabits-core/src/main/java/org/isoron/uhabits/core/models/CheckmarkList.java @@ -349,6 +349,12 @@ public abstract class CheckmarkList add(buildCheckmarksFromIntervals(reps, intervals)); } + public List getAll() { + Repetition oldest = habit.getRepetitions().getOldest(); + if(oldest == null) return new ArrayList<>(); + return getByInterval(oldest.getTimestamp(), DateUtils.getToday()); + } + static final class Interval { final Timestamp begin; @@ -408,9 +414,7 @@ public abstract class CheckmarkList @NonNull public List groupBy(DateUtils.TruncateField field) { - Repetition oldest = habit.getRepetitions().getOldest(); - if(oldest == null) return new ArrayList<>(); - List checks = getByInterval(oldest.getTimestamp(), DateUtils.getToday()); + List checks = getAll(); int count = 0; Timestamp truncatedTimestamps[] = new Timestamp[checks.size()];