diff --git a/build.gradle b/build.gradle index 5594e43b5..90157a8f7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java' +apply plugin: 'maven-publish' apply from: 'gradle-mvn-push.gradle' targetCompatibility = '1.6' diff --git a/src/com/activeandroid/ActiveAndroid.java b/src/com/activeandroid/ActiveAndroid.java index c58c8efd8..60237c8f8 100644 --- a/src/com/activeandroid/ActiveAndroid.java +++ b/src/com/activeandroid/ActiveAndroid.java @@ -20,6 +20,7 @@ import android.database.sqlite.SQLiteDatabase; import com.activeandroid.util.Log; +import android.os.Build; public final class ActiveAndroid { ////////////////////////////////////////////////////////////////////////////////////// @@ -60,8 +61,16 @@ public static SQLiteDatabase getDatabase() { return Cache.openDatabase(); } + /** + * Non-exclusive transactions allows BEGIN IMMEDIATE + * blocks, allowing better read concurrency. + */ public static void beginTransaction() { - Cache.openDatabase().beginTransaction(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + Cache.openDatabase().beginTransaction(); + } else { + Cache.openDatabase().beginTransactionNonExclusive(); + } } public static void endTransaction() { diff --git a/src/com/activeandroid/DatabaseHelper.java b/src/com/activeandroid/DatabaseHelper.java index cc3bebec7..77072dfd7 100644 --- a/src/com/activeandroid/DatabaseHelper.java +++ b/src/com/activeandroid/DatabaseHelper.java @@ -38,6 +38,7 @@ import com.activeandroid.util.NaturalOrderComparator; import com.activeandroid.util.SQLiteUtils; import com.activeandroid.util.SqlParser; +import android.os.Build; public final class DatabaseHelper extends SQLiteOpenHelper { ////////////////////////////////////////////////////////////////////////////////////// @@ -66,6 +67,25 @@ public DatabaseHelper(Configuration configuration) { // OVERRIDEN METHODS ////////////////////////////////////////////////////////////////////////////////////// + /** + * onConfigure is called when the db connection + * is being configured. It's the right place + * to enable write-ahead logging or foreign + * key support. + * + * Available for API level 16 (JellyBean) and above. + */ + @Override + public void onConfigure(SQLiteDatabase db) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + db.enableWriteAheadLogging(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + db.setForeignKeyConstraintsEnabled(true); + } + executePragmas(db); + } + @Override public void onOpen(SQLiteDatabase db) { executePragmas(db); diff --git a/src/com/activeandroid/Model.java b/src/com/activeandroid/Model.java index 276b10c9c..fe1b14b87 100644 --- a/src/com/activeandroid/Model.java +++ b/src/com/activeandroid/Model.java @@ -293,10 +293,6 @@ else if (ReflectionUtils.isSubclassOf(fieldType, Enum.class)) { Log.e(e.getClass().getName(), e); } } - - if (mId != null) { - Cache.addEntity(this); - } } diff --git a/src/com/activeandroid/ModelInfo.java b/src/com/activeandroid/ModelInfo.java index f983cacf8..ac50fc87c 100644 --- a/src/com/activeandroid/ModelInfo.java +++ b/src/com/activeandroid/ModelInfo.java @@ -29,6 +29,8 @@ import java.util.Map; import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; import com.activeandroid.serializer.CalendarSerializer; import com.activeandroid.serializer.FileSerializer; @@ -41,6 +43,12 @@ import dalvik.system.DexFile; final class ModelInfo { + private static final String PREFS_FILE = "multidex.version"; + private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; + private static final String EXTRACTED_NAME_EXT = ".classes"; + private static final String KEY_DEX_NUMBER = "dex.number"; + private static final String EXTRACTED_SUFFIX = ".zip"; + ////////////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS ////////////////////////////////////////////////////////////////////////////////////// @@ -125,28 +133,41 @@ private boolean loadModelFromMetaData(Configuration configuration) { 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(); + try { + for (String sourcePath : getSourcePaths(context)) { + try { + if (sourcePath != null && !(new File(sourcePath).isDirectory())) { + DexFile dexfile; + if (sourcePath.endsWith(EXTRACTED_SUFFIX)) + dexfile = DexFile.loadDex(sourcePath, sourcePath + ".tmp", 0); + else + 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 (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); + while (resources.hasMoreElements()) { + String path = resources.nextElement().getFile(); + if (path.contains("bin") || path.contains("classes")) { + paths.add(path); + } + } + } + } catch (Exception e) { + e.printStackTrace(); } } + } catch (Exception e) { + e.printStackTrace(); } for (String path : paths) { @@ -155,6 +176,34 @@ private void scanForModel(Context context) throws IOException { } } + private static SharedPreferences getMultiDexPreferences(Context context) { + int mode = Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS; + return context.getSharedPreferences(PREFS_FILE, mode); + } + + private static List getSourcePaths(Context context) throws Exception { + ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + File sourceApk = new File(applicationInfo.sourceDir); + File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); + + List sourcePaths = new ArrayList(); + sourcePaths.add(applicationInfo.sourceDir); + + String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; + int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1); + + for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) { + String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; + File extractedFile = new File(dexDir, fileName); + if (extractedFile.isFile()) + sourcePaths.add(extractedFile.getAbsolutePath()); + else + throw new Exception("Missing extracted secondary dex file '" + extractedFile.getPath() + "'"); + } + + return sourcePaths; + } + private void scanForModelClasses(File path, String packageName, ClassLoader classLoader) { if (path.isDirectory()) { for (File file : path.listFiles()) { diff --git a/src/com/activeandroid/automigration/TableDifference.java b/src/com/activeandroid/automigration/TableDifference.java index d7356985b..78c92814c 100644 --- a/src/com/activeandroid/automigration/TableDifference.java +++ b/src/com/activeandroid/automigration/TableDifference.java @@ -1,5 +1,7 @@ package com.activeandroid.automigration; +import android.util.Log; + import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; @@ -39,7 +41,12 @@ public TableDifference(TableInfo tableInfo, SQLTableInfo sqlTableInfo) { if (existingColumnInfo.getType() == sqlColumnInfo.getType()) { mDifferences.put(sqlColumnInfo, existingColumnInfo); } else { - throw new IncompatibleColumnTypesException(tableInfo.getTableName(), existingColumnInfo.getName(), existingColumnInfo.getType(), sqlColumnInfo.getType()); + // allow column type changes just to let SQLite attempt to cast these values + Log.w(TableDifference.class.getName(), "potentially incompatible column types (table='" + + tableInfo.getTableName() + "' column='" + existingColumnInfo.getName() + + "' current type='" + existingColumnInfo.getType() + "' new type='" + + sqlColumnInfo.getType() + "')"); + mDifferences.put(sqlColumnInfo, existingColumnInfo); } } break; diff --git a/src/com/activeandroid/internal/AnnotationProcessor.java b/src/com/activeandroid/internal/AnnotationProcessor.java index d9b3dc3ea..94c359d6b 100644 --- a/src/com/activeandroid/internal/AnnotationProcessor.java +++ b/src/com/activeandroid/internal/AnnotationProcessor.java @@ -16,6 +16,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileObject; @@ -134,7 +135,8 @@ private void generate(TypeElement tableElement, Set columns) { private String getLoadFromCursorCode(Set columns) { StringBuilder stringBuilder = new StringBuilder(); - + stringBuilder.append(" int i = -1; // column index \n"); + final String nullCheck = CURSOR + ".isNull(i) ? null : "; for (VariableElement column : columns) { Column annotation = column.getAnnotation(Column.class); @@ -147,40 +149,56 @@ private String getLoadFromCursorCode(Set columns) { boolean notPrimitiveType = typeMirror instanceof DeclaredType; String type = typeMirror.toString() + ".class"; String getColumnIndex = COLUMNS_ORDERED + ".indexOf(\"" + fieldName + "\")"; + String getColumnIndexAssignment = "i = " + getColumnIndex + "; \n"; + stringBuilder.append(" " + getColumnIndexAssignment ); if (notPrimitiveType) { stringBuilder.append(" if (ModelHelper.isSerializable(" + type + ")) {\n"); - stringBuilder.append(" " + MODEL + "." + column.getSimpleName() + " = (" + typeMirror.toString() + ") ModelHelper.getSerializable(cursor, " + type + ", " + getColumnIndex + ");\n"); + stringBuilder.append(" " + MODEL + "." + column.getSimpleName() + " = (" + typeMirror.toString() + ") ModelHelper.getSerializable(cursor, " + type + ", i);\n"); stringBuilder.append(" } else {\n"); stringBuilder.append(" " + MODEL + "." + column.getSimpleName() + " = "); } else { stringBuilder.append(" " + MODEL + "." + column.getSimpleName() + " = "); } - if (isTypeOf(typeMirror, Integer.class) || isTypeOf(typeMirror, int.class)) - stringBuilder.append(CURSOR + ".getInt(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Byte.class) || isTypeOf(typeMirror, byte.class)) - stringBuilder.append(CURSOR + ".getInt(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Short.class) || isTypeOf(typeMirror, short.class)) - stringBuilder.append(CURSOR + ".getInt(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Long.class) || isTypeOf(typeMirror, long.class)) - stringBuilder.append(CURSOR + ".getLong(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Float.class) || isTypeOf(typeMirror, float.class)) - stringBuilder.append(CURSOR + ".getFloat(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Double.class) || isTypeOf(typeMirror, double.class)) - stringBuilder.append(CURSOR + ".getDouble(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Boolean.class) || isTypeOf(typeMirror, boolean.class)) - stringBuilder.append(CURSOR + ".getInt(" + getColumnIndex + ") != 0;\n"); - else if (isTypeOf(typeMirror, Character.class) || isTypeOf(typeMirror, char.class)) - stringBuilder.append(CURSOR + ".getString(" + getColumnIndex + ");\n"); + if (isTypeOf(typeMirror, Integer.class) || isTypeOf(typeMirror, Byte.class) || isTypeOf(typeMirror, Short.class) ) + stringBuilder.append(nullCheck).append(CURSOR + ".getInt(i);\n"); + else if (isTypeOf(typeMirror, Long.class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getLong(i);\n"); + else if (isTypeOf(typeMirror, Float.class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getFloat(i);\n"); + else if (isTypeOf(typeMirror, Double.class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getDouble(i);\n"); + else if (isTypeOf(typeMirror, int.class)) + stringBuilder.append(CURSOR + ".getInt(i);\n"); + else if (isTypeOf(typeMirror, byte.class)) + stringBuilder.append(CURSOR + ".getInt(i);\n"); + else if (isTypeOf(typeMirror, short.class)) + stringBuilder.append(CURSOR + ".getInt(i);\n"); + else if (isTypeOf(typeMirror, long.class)) + stringBuilder.append(CURSOR + ".getLong(i);\n"); + else if (isTypeOf(typeMirror, float.class)) + stringBuilder.append(CURSOR + ".getFloat(i);\n"); + else if (isTypeOf(typeMirror, double.class)) + stringBuilder.append(CURSOR + ".getDouble(i);\n"); + else if (isTypeOf(typeMirror, Boolean.class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getInt(i) != 0;\n"); + else if (isTypeOf(typeMirror, boolean.class)) + stringBuilder.append(CURSOR + ".getInt(i) != 0;\n"); + else if (isTypeOf(typeMirror, char.class)) + stringBuilder.append(CURSOR + ".getString(i);\n"); + else if (isTypeOf(typeMirror, Character.class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getString(i);\n"); else if (isTypeOf(typeMirror, String.class)) - stringBuilder.append(CURSOR + ".getString(" + getColumnIndex + ");\n"); - else if (isTypeOf(typeMirror, Byte[].class) || isTypeOf(typeMirror, byte[].class)) - stringBuilder.append(CURSOR + ".getBlob(" + getColumnIndex + ");\n"); + stringBuilder.append(nullCheck).append(CURSOR + ".getString(i);\n"); + else if (isTypeOf(typeMirror, byte[].class)) + stringBuilder.append(CURSOR + ".getBlob(i);\n"); + else if (isTypeOf(typeMirror, Byte[].class)) + stringBuilder.append(nullCheck).append(CURSOR + ".getBlob(i);\n"); else if (isTypeOf(typeMirror, Model.class)) - stringBuilder.append("(" + typeMirror.toString() + ") ModelHelper.getModel(cursor, " + type + ", " + getColumnIndex + ");\n"); + stringBuilder.append("(" + typeMirror.toString() + ") ModelHelper.getModel(cursor, " + type + ", i);\n"); else if (isTypeOf(typeMirror, Enum.class)) - stringBuilder.append("(" + typeMirror.toString() + ") ModelHelper.getEnum(cursor, " + type + ", " + getColumnIndex + ");\n"); + stringBuilder.append("(" + typeMirror.toString() + ") ModelHelper.getEnum(cursor, " + type + ", i);\n"); else stringBuilder.append(" null;\n"); if (notPrimitiveType) { @@ -205,7 +223,7 @@ private String getFillContentValuesCode(Set columns) { boolean notPrimitiveType = typeMirror instanceof DeclaredType; String type = typeMirror.toString() + ".class"; String getValue = MODEL + "." + column.getSimpleName(); - + if (notPrimitiveType) { stringBuilder.append(" if (ModelHelper.isSerializable(" + type + ")) {\n"); stringBuilder.append(" ModelHelper.setSerializable(" + CONTENT_VALUES + ", " + type + ", " + getValue + ", \"" + fieldName + "\");\n"); @@ -256,6 +274,9 @@ private boolean isTypeOf(TypeMirror typeMirror, Class type) { if (type.getName().equals(typeMirror.toString())) return true; + if ((typeMirror.getKind() == TypeKind.ARRAY) && type.isArray()) + return typeMirror.toString().equals(type.getComponentType() + "[]"); + if (typeMirror instanceof DeclaredType == false) return false; diff --git a/src/com/activeandroid/query/From.java b/src/com/activeandroid/query/From.java index ab3837a90..f2f1cea91 100644 --- a/src/com/activeandroid/query/From.java +++ b/src/com/activeandroid/query/From.java @@ -40,6 +40,7 @@ public final class From implements Sqlable { private String mOrderBy; private String mLimit; private String mOffset; + private boolean useCache = true; private List mArguments; @@ -295,7 +296,7 @@ public String toCountSql() { public List execute() { if (mQueryBase instanceof Select) { - return SQLiteUtils.rawQuery(mType, toSql(), getArguments()); + return SQLiteUtils.rawQuery(mType, toSql(), getArguments(), useCache); } else { SQLiteUtils.execSql(toSql(), getArguments()); @@ -308,7 +309,7 @@ public List execute() { public T executeSingle() { if (mQueryBase instanceof Select) { limit(1); - return (T) SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments()); + return (T) SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments(), useCache); } else { limit(1); @@ -343,4 +344,15 @@ public String[] getArguments() { return args; } + + /** + * Retrieve entities from ActiveAndroid's cache. The cache is enabled by default. + * @param useCache + * @return + */ + public From setUseCache(boolean useCache) { + this.useCache = useCache; + return this; + } + } diff --git a/src/com/activeandroid/util/SQLiteUtils.java b/src/com/activeandroid/util/SQLiteUtils.java index c4b545505..ba0bd2e91 100644 --- a/src/com/activeandroid/util/SQLiteUtils.java +++ b/src/com/activeandroid/util/SQLiteUtils.java @@ -100,13 +100,17 @@ public static void execSql(String sql, Object[] bindArgs) { Cache.openDatabase().execSQL(sql, bindArgs); } - public static List rawQuery(Class type, String sql, String[] selectionArgs) { + public static List rawQuery(Class type, String sql, String[] selectionArgs, boolean useCache) { Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs); - List entities = processCursor(type, cursor); + List entities = processCursor(type, cursor, useCache); cursor.close(); return entities; } + + public static List rawQuery(Class type, String sql, String[] selectionArgs){ + return rawQuery(type, sql, selectionArgs, true); + } public static int intQuery(final String sql, final String[] selectionArgs) { final Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs); @@ -116,8 +120,8 @@ public static int intQuery(final String sql, final String[] selectionArgs) { return number; } - public static T rawQuerySingle(Class type, String sql, String[] selectionArgs) { - List entities = rawQuery(type, sql, selectionArgs); + public static T rawQuerySingle(Class type, String sql, String[] selectionArgs, boolean useCache) { + List entities = rawQuery(type, sql, selectionArgs, useCache); if (entities.size() > 0) { return entities.get(0); @@ -126,6 +130,10 @@ public static T rawQuerySingle(Class type, St return null; } + public static T rawQuerySingle(Class type, String sql, String[] selectionArgs){ + return rawQuerySingle(type, sql, selectionArgs, true); + } + // Database creation public static ArrayList createUniqueDefinition(TableInfo tableInfo) { @@ -358,7 +366,7 @@ else if (ReflectionUtils.isSubclassOf(type, Enum.class)) { } @SuppressWarnings("unchecked") - public static List processCursor(Class type, Cursor cursor) { + public static List processCursor(Class type, Cursor cursor, boolean useCache) { TableInfo tableInfo = Cache.getTableInfo(type); String idName = tableInfo.getIdName(); final List entities = new ArrayList(); @@ -373,13 +381,24 @@ public static List processCursor(Class typ */ List columnsOrdered = new ArrayList(Arrays.asList(cursor.getColumnNames())); do { - Model entity = Cache.getEntity(type, cursor.getLong(columnsOrdered.indexOf(idName))); + Model entity; + + if(useCache) { + entity = Cache.getEntity(type, cursor.getLong(columnsOrdered.indexOf(idName))); + } else { + entity = null; + } + if (entity == null) { entity = (T) entityConstructor.newInstance(); } entity.loadFromCursor(cursor); entities.add((T) entity); + + // add to cache + if (useCache && entity.getId() != null) + Cache.addEntity(entity); } while (cursor.moveToNext()); } @@ -402,6 +421,10 @@ public static List processCursor(Class typ return entities; } + public static List processCursor(Class type, Cursor cursor){ + return processCursor(type, cursor, true); + } + private static int processIntCursor(final Cursor cursor) { if (cursor.moveToFirst()) { return cursor.getInt(0); diff --git a/tests/src/com/activeandroid/test/ModelTest.java b/tests/src/com/activeandroid/test/ModelTest.java index a86ad0b0f..1ae136599 100644 --- a/tests/src/com/activeandroid/test/ModelTest.java +++ b/tests/src/com/activeandroid/test/ModelTest.java @@ -232,6 +232,16 @@ public void testJoinWithSameNames() { } + public void testNonCachedReads(){ + MockModel cached = new MockModel(); + cached.save(); + + MockModel notCached = new Select().from(MockModel.class).where("id = ?", cached.getId()) + .setUseCache(false).executeSingle(); + + assertTrue(cached!=notCached); + } + /** * Mock model as we need 2 different model classes. */