/* * Copyright 2016 requery.io * * 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. */ package io.requery.android.sqlite; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; import android.support.annotation.Nullable; import android.text.TextUtils; import io.requery.android.DefaultMapping; import io.requery.android.LoggingListener; import io.requery.meta.EntityModel; import io.requery.sql.Configuration; import io.requery.sql.ConfigurationBuilder; import io.requery.sql.Mapping; import io.requery.sql.Platform; import io.requery.sql.SchemaModifier; import io.requery.sql.TableCreationMode; import io.requery.sql.platform.SQLite; import io.requery.util.function.Function; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLNonTransientConnectionException; /** * Wrapper for working with Android SQLite databases. This provides a {@link Connection} wrapping * the standard java SQLite API turning it into standard JDBC APIs. * * <p> This class extends the standard {@link SQLiteOpenHelper} and is used to create the database. * {@link #onCreate(SQLiteDatabase)} will create the database tables & columns automatically. * * <p> {@link #onUpgrade(SQLiteDatabase, int, int)} will create any missing tables/columns, however * it will not remove any tables/columns more complex upgrades should be handled by overriding * {@link #onUpgrade(SQLiteDatabase, int, int)} and implementing a script to handle the migration. * * @author Nikhil Purushe */ @SuppressWarnings("WeakerAccess") public class DatabaseSource extends SQLiteOpenHelper implements DatabaseProvider<SQLiteDatabase> { private final Platform platform; private final EntityModel model; private Mapping mapping; private SQLiteDatabase db; private Configuration configuration; private boolean configured; private boolean loggingEnabled; private TableCreationMode mode; /** * Creates a new {@link DatabaseSource} instance. * * @param context context * @param model the entity model * @param version the schema version */ public DatabaseSource(Context context, EntityModel model, int version) { this(context, model, getDefaultDatabaseName(context, model), null, version); } /** * Creates a new {@link DatabaseSource} instance. * * @param context context * @param model the entity model * @param name database filename * @param version the schema version */ public DatabaseSource(Context context, EntityModel model, @Nullable String name, int version) { this(context, model, name, null, version); } /** * Creates a new {@link DatabaseSource} instance. * * @param context context * @param model the entity model * @param name database filename * @param factory optional {@link android.database.sqlite.SQLiteDatabase.CursorFactory} * @param version the schema version */ public DatabaseSource(Context context, EntityModel model, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) { this(context, model, name, factory, version, new SQLite()); } /** * Creates a new {@link DatabaseSource} instance. * * @param context context * @param model the entity model * @param name database filename * @param factory optional {@link android.database.sqlite.SQLiteDatabase.CursorFactory} * @param version the schema version * @param platform platform instance */ public DatabaseSource(Context context, EntityModel model, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version, SQLite platform) { super(context, name, factory, version); if (model == null) { throw new IllegalArgumentException("null model"); } this.platform = platform; this.model = model; this.mode = TableCreationMode.CREATE_NOT_EXISTS; } @Override public void setLoggingEnabled(boolean enable) { this.loggingEnabled = enable; } @Override public void setTableCreationMode(TableCreationMode mode) { this.mode = mode; } private static String getDefaultDatabaseName(Context context, EntityModel model) { return TextUtils.isEmpty(model.getName()) ? context.getPackageName() : model.getName(); } /** * Override to change the default {@link Mapping}. * * @param platform platform instance * @return the configured mapping. */ protected Mapping onCreateMapping(Platform platform) { return new DefaultMapping(platform); } /** * Override to modify the configuration before it is built. * * @param builder {@link ConfigurationBuilder instance} to configure. */ protected void onConfigure(ConfigurationBuilder builder) { if (loggingEnabled) { LoggingListener loggingListener = new LoggingListener(); builder.addStatementListener(loggingListener); } } private Connection getConnection(SQLiteDatabase db) throws SQLException { synchronized (this) { if (!db.isOpen()) { throw new SQLNonTransientConnectionException(); } return new SqliteConnection(db); } } @Override public Configuration getConfiguration() { if (mapping == null) { mapping = onCreateMapping(platform); } if (mapping == null) { throw new IllegalStateException(); } if (configuration == null) { ConfigurationBuilder builder = new ConfigurationBuilder(this, model) .setMapping(mapping) .setPlatform(platform) .setBatchUpdateSize(1000); onConfigure(builder); configuration = builder.build(); } return configuration; } @Override public void onCreate(SQLiteDatabase db) { this.db = db; new SchemaModifier(getConfiguration()).createTables(TableCreationMode.CREATE); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); if (!db.isReadOnly()) { db.setForeignKeyConstraintsEnabled(true); } } @Override public void onUpgrade(final SQLiteDatabase db, int oldVersion, int newVersion) { this.db = db; SchemaUpdater updater = new SchemaUpdater(getConfiguration(), new Function<String, Cursor>() { @Override public Cursor apply(String s) { return db.rawQuery(s, null); } }, mode); updater.update(); } @Override public Connection getConnection() throws SQLException { synchronized (this) { if (db == null) { db = getWritableDatabase(); } if (!configured && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { db.execSQL("PRAGMA foreign_keys = ON"); long pageSize = db.getPageSize(); // on later Android versions page size was correctly set to 4096, // do the same for older versions if (pageSize == 1024) { db.setPageSize(4096); } configured = true; } return getConnection(db); } } }