├── AssetSQLiteOpenHelper.java ├── AssetSQLiteOpenHelperFactory.java ├── FrameworkSQLiteDatabase.java ├── FrameworkSQLiteProgram.java ├── FrameworkSQLiteStatement.java ├── LICENSE ├── SQLiteAssetHelper.java ├── Utils.java └── VersionComparator.java /AssetSQLiteOpenHelper.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | 3 | import android.arch.persistence.db.SupportSQLiteDatabase; 4 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 5 | import android.content.Context; 6 | import android.database.DatabaseErrorHandler; 7 | import android.database.sqlite.SQLiteDatabase; 8 | import android.os.Build; 9 | import android.support.annotation.RequiresApi; 10 | 11 | class AssetSQLiteOpenHelper implements SupportSQLiteOpenHelper { 12 | private final OpenHelper mDelegate; 13 | 14 | AssetSQLiteOpenHelper(Context context, String name, 15 | SQLiteDatabase.CursorFactory factory, int version, 16 | DatabaseErrorHandler errorHandler, 17 | Callback callback) { 18 | mDelegate = createDelegate(context, name, factory, version, errorHandler, callback); 19 | } 20 | 21 | private OpenHelper createDelegate(Context context, String name, 22 | SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler, 23 | final Callback callback) { 24 | return new OpenHelper(context, name, factory, version, errorHandler) { 25 | @Override 26 | public final void onCreate(SQLiteDatabase sqLiteDatabase) { 27 | mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); 28 | callback.onCreate(mWrappedDb); 29 | } 30 | 31 | @Override 32 | public final void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { 33 | callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); 34 | } 35 | 36 | @Override 37 | public final void onConfigure(SQLiteDatabase db) { 38 | callback.onConfigure(getWrappedDb(db)); 39 | } 40 | 41 | @Override 42 | public final void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 43 | callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); 44 | } 45 | 46 | @Override 47 | public void onOpen(SQLiteDatabase db) { 48 | callback.onOpen(getWrappedDb(db)); 49 | } 50 | }; 51 | } 52 | 53 | @Override 54 | public String getDatabaseName() { 55 | return mDelegate.getDatabaseName(); 56 | } 57 | 58 | @Override 59 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 60 | public void setWriteAheadLoggingEnabled(boolean enabled) { 61 | mDelegate.setWriteAheadLoggingEnabled(enabled); 62 | } 63 | 64 | @Override 65 | public SupportSQLiteDatabase getWritableDatabase() { 66 | return mDelegate.getWritableSupportDatabase(); 67 | } 68 | 69 | @Override 70 | public SupportSQLiteDatabase getReadableDatabase() { 71 | return mDelegate.getReadableSupportDatabase(); 72 | } 73 | 74 | @Override 75 | public void close() { 76 | mDelegate.close(); 77 | } 78 | 79 | abstract static class OpenHelper extends SQLiteAssetHelper { 80 | 81 | FrameworkSQLiteDatabase mWrappedDb; 82 | 83 | OpenHelper(Context context, String name, 84 | SQLiteDatabase.CursorFactory factory, int version, 85 | DatabaseErrorHandler errorHandler) { 86 | super(context, name, null, factory, version, errorHandler); 87 | } 88 | 89 | SupportSQLiteDatabase getWritableSupportDatabase() { 90 | SQLiteDatabase db = super.getWritableDatabase(); 91 | return getWrappedDb(db); 92 | } 93 | 94 | SupportSQLiteDatabase getReadableSupportDatabase() { 95 | SQLiteDatabase db = super.getReadableDatabase(); 96 | return getWrappedDb(db); 97 | } 98 | 99 | FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) { 100 | if (mWrappedDb == null) { 101 | mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); 102 | } 103 | return mWrappedDb; 104 | } 105 | 106 | @Override 107 | public synchronized void close() { 108 | super.close(); 109 | mWrappedDb = null; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /AssetSQLiteOpenHelperFactory.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | 3 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 4 | 5 | /** 6 | * Implements {@link SupportSQLiteOpenHelper.Factory} using the SQLite implementation in the 7 | * framework. 8 | */ 9 | @SuppressWarnings("unused") 10 | public class AssetSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { 11 | @Override 12 | public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { 13 | return new AssetSQLiteOpenHelper( 14 | configuration.context, configuration.name, null, 15 | configuration.version, configuration.errorHandler, configuration.callback 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FrameworkSQLiteDatabase.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset;/* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import android.arch.persistence.db.SimpleSQLiteQuery; 19 | import android.arch.persistence.db.SupportSQLiteDatabase; 20 | import android.arch.persistence.db.SupportSQLiteQuery; 21 | import android.arch.persistence.db.SupportSQLiteStatement; 22 | import android.content.ContentValues; 23 | import android.database.Cursor; 24 | import android.database.SQLException; 25 | import android.database.sqlite.SQLiteCursor; 26 | import android.database.sqlite.SQLiteDatabase; 27 | import android.database.sqlite.SQLiteTransactionListener; 28 | import android.os.Build; 29 | import android.os.CancellationSignal; 30 | import android.support.annotation.RequiresApi; 31 | import android.util.Pair; 32 | 33 | import java.io.IOException; 34 | import java.util.List; 35 | import java.util.Locale; 36 | 37 | /** 38 | * Delegates all calls to an implementation of {@link SQLiteDatabase}. 39 | */ 40 | @SuppressWarnings("unused") 41 | class FrameworkSQLiteDatabase implements SupportSQLiteDatabase { 42 | private static final String[] CONFLICT_VALUES = new String[] 43 | {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; 44 | private static final String[] EMPTY_STRING_ARRAY = new String[0]; 45 | 46 | private final SQLiteDatabase mDelegate; 47 | 48 | /** 49 | * Creates a wrapper around {@link SQLiteDatabase}. 50 | * 51 | * @param delegate The delegate to receive all calls. 52 | */ 53 | @SuppressWarnings("WeakerAccess") 54 | public FrameworkSQLiteDatabase(SQLiteDatabase delegate) { 55 | mDelegate = delegate; 56 | } 57 | 58 | @Override 59 | public SupportSQLiteStatement compileStatement(String sql) { 60 | return new FrameworkSQLiteStatement(mDelegate.compileStatement(sql)); 61 | } 62 | 63 | @Override 64 | public void beginTransaction() { 65 | mDelegate.beginTransaction(); 66 | } 67 | 68 | @Override 69 | public void beginTransactionNonExclusive() { 70 | mDelegate.beginTransactionNonExclusive(); 71 | } 72 | 73 | @Override 74 | public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { 75 | mDelegate.beginTransactionWithListener(transactionListener); 76 | } 77 | 78 | @Override 79 | public void beginTransactionWithListenerNonExclusive( 80 | SQLiteTransactionListener transactionListener) { 81 | mDelegate.beginTransactionWithListenerNonExclusive(transactionListener); 82 | } 83 | 84 | @Override 85 | public void endTransaction() { 86 | mDelegate.endTransaction(); 87 | } 88 | 89 | @Override 90 | public void setTransactionSuccessful() { 91 | mDelegate.setTransactionSuccessful(); 92 | } 93 | 94 | @Override 95 | public boolean inTransaction() { 96 | return mDelegate.inTransaction(); 97 | } 98 | 99 | @Override 100 | public boolean isDbLockedByCurrentThread() { 101 | return mDelegate.isDbLockedByCurrentThread(); 102 | } 103 | 104 | @Override 105 | public boolean yieldIfContendedSafely() { 106 | return mDelegate.yieldIfContendedSafely(); 107 | } 108 | 109 | @Override 110 | public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { 111 | return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay); 112 | } 113 | 114 | @Override 115 | public int getVersion() { 116 | return mDelegate.getVersion(); 117 | } 118 | 119 | @Override 120 | public void setVersion(int version) { 121 | mDelegate.setVersion(version); 122 | } 123 | 124 | @Override 125 | public long getMaximumSize() { 126 | return mDelegate.getMaximumSize(); 127 | } 128 | 129 | @Override 130 | public long setMaximumSize(long numBytes) { 131 | return mDelegate.setMaximumSize(numBytes); 132 | } 133 | 134 | @Override 135 | public long getPageSize() { 136 | return mDelegate.getPageSize(); 137 | } 138 | 139 | @Override 140 | public void setPageSize(long numBytes) { 141 | mDelegate.setPageSize(numBytes); 142 | } 143 | 144 | @Override 145 | public Cursor query(String query) { 146 | return query(new SimpleSQLiteQuery(query)); 147 | } 148 | 149 | @Override 150 | public Cursor query(String query, Object[] bindArgs) { 151 | return query(new SimpleSQLiteQuery(query, bindArgs)); 152 | } 153 | 154 | 155 | @Override 156 | public Cursor query(final SupportSQLiteQuery supportQuery) { 157 | return mDelegate.rawQueryWithFactory((db, masterQuery, editTable, query) -> { 158 | supportQuery.bindTo(new FrameworkSQLiteProgram(query)); 159 | return new SQLiteCursor(masterQuery, editTable, query); 160 | }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null); 161 | } 162 | 163 | @Override 164 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 165 | public Cursor query(final SupportSQLiteQuery supportQuery, 166 | CancellationSignal cancellationSignal) { 167 | return mDelegate.rawQueryWithFactory((db, masterQuery, editTable, query) -> { 168 | supportQuery.bindTo(new FrameworkSQLiteProgram(query)); 169 | return new SQLiteCursor(masterQuery, editTable, query); 170 | }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null, cancellationSignal); 171 | } 172 | 173 | @Override 174 | public long insert(String table, int conflictAlgorithm, ContentValues values) 175 | throws SQLException { 176 | return mDelegate.insertWithOnConflict(table, null, values, 177 | conflictAlgorithm); 178 | } 179 | 180 | @Override 181 | public int delete(String table, String whereClause, Object[] whereArgs) { 182 | String query = "DELETE FROM " + table 183 | + (isEmpty(whereClause) ? "" : " WHERE " + whereClause); 184 | SupportSQLiteStatement statement = compileStatement(query); 185 | SimpleSQLiteQuery.bind(statement, whereArgs); 186 | return statement.executeUpdateDelete(); 187 | } 188 | 189 | 190 | @Override 191 | public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause, 192 | Object[] whereArgs) { 193 | // taken from SQLiteDatabase class. 194 | if (values == null || values.size() == 0) { 195 | throw new IllegalArgumentException("Empty values"); 196 | } 197 | StringBuilder sql = new StringBuilder(120); 198 | sql.append("UPDATE "); 199 | sql.append(CONFLICT_VALUES[conflictAlgorithm]); 200 | sql.append(table); 201 | sql.append(" SET "); 202 | 203 | // move all bind args to one array 204 | int setValuesSize = values.size(); 205 | int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); 206 | Object[] bindArgs = new Object[bindArgsSize]; 207 | int i = 0; 208 | for (String colName : values.keySet()) { 209 | sql.append((i > 0) ? "," : ""); 210 | sql.append(colName); 211 | bindArgs[i++] = values.get(colName); 212 | sql.append("=?"); 213 | } 214 | if (whereArgs != null) { 215 | for (i = setValuesSize; i < bindArgsSize; i++) { 216 | bindArgs[i] = whereArgs[i - setValuesSize]; 217 | } 218 | } 219 | if (!isEmpty(whereClause)) { 220 | sql.append(" WHERE "); 221 | sql.append(whereClause); 222 | } 223 | SupportSQLiteStatement stmt = compileStatement(sql.toString()); 224 | SimpleSQLiteQuery.bind(stmt, bindArgs); 225 | return stmt.executeUpdateDelete(); 226 | } 227 | 228 | @Override 229 | public void execSQL(String sql) throws SQLException { 230 | mDelegate.execSQL(sql); 231 | } 232 | 233 | @Override 234 | public void execSQL(String sql, Object[] bindArgs) throws SQLException { 235 | mDelegate.execSQL(sql, bindArgs); 236 | } 237 | 238 | @Override 239 | public boolean isReadOnly() { 240 | return mDelegate.isReadOnly(); 241 | } 242 | 243 | @Override 244 | public boolean isOpen() { 245 | return mDelegate.isOpen(); 246 | } 247 | 248 | @Override 249 | public boolean needUpgrade(int newVersion) { 250 | return mDelegate.needUpgrade(newVersion); 251 | } 252 | 253 | @Override 254 | public String getPath() { 255 | return mDelegate.getPath(); 256 | } 257 | 258 | @Override 259 | public void setLocale(Locale locale) { 260 | mDelegate.setLocale(locale); 261 | } 262 | 263 | @Override 264 | public void setMaxSqlCacheSize(int cacheSize) { 265 | mDelegate.setMaxSqlCacheSize(cacheSize); 266 | } 267 | 268 | @Override 269 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 270 | public void setForeignKeyConstraintsEnabled(boolean enable) { 271 | mDelegate.setForeignKeyConstraintsEnabled(enable); 272 | } 273 | 274 | @Override 275 | public boolean enableWriteAheadLogging() { 276 | return mDelegate.enableWriteAheadLogging(); 277 | } 278 | 279 | @Override 280 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 281 | public void disableWriteAheadLogging() { 282 | mDelegate.disableWriteAheadLogging(); 283 | } 284 | 285 | @Override 286 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) 287 | public boolean isWriteAheadLoggingEnabled() { 288 | return mDelegate.isWriteAheadLoggingEnabled(); 289 | } 290 | 291 | @Override 292 | public List> getAttachedDbs() { 293 | return mDelegate.getAttachedDbs(); 294 | } 295 | 296 | @Override 297 | public boolean isDatabaseIntegrityOk() { 298 | return mDelegate.isDatabaseIntegrityOk(); 299 | } 300 | 301 | @Override 302 | public void close() throws IOException { 303 | mDelegate.close(); 304 | } 305 | 306 | private static boolean isEmpty(String input) { 307 | return input == null || input.length() == 0; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /FrameworkSQLiteProgram.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | 3 | import android.arch.persistence.db.SupportSQLiteProgram; 4 | import android.database.sqlite.SQLiteProgram; 5 | 6 | /** 7 | * An wrapper around {@link SQLiteProgram} to implement {@link SupportSQLiteProgram} API. 8 | */ 9 | class FrameworkSQLiteProgram implements SupportSQLiteProgram { 10 | private final SQLiteProgram mDelegate; 11 | 12 | FrameworkSQLiteProgram(SQLiteProgram delegate) { 13 | mDelegate = delegate; 14 | } 15 | 16 | @Override 17 | public void bindNull(int index) { 18 | mDelegate.bindNull(index); 19 | } 20 | 21 | @Override 22 | public void bindLong(int index, long value) { 23 | mDelegate.bindLong(index, value); 24 | } 25 | 26 | @Override 27 | public void bindDouble(int index, double value) { 28 | mDelegate.bindDouble(index, value); 29 | } 30 | 31 | @Override 32 | public void bindString(int index, String value) { 33 | mDelegate.bindString(index, value); 34 | } 35 | 36 | @Override 37 | public void bindBlob(int index, byte[] value) { 38 | mDelegate.bindBlob(index, value); 39 | } 40 | 41 | @Override 42 | public void clearBindings() { 43 | mDelegate.clearBindings(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /FrameworkSQLiteStatement.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | /* 3 | * Copyright (C) 2016 The Android Open Source Project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | import android.arch.persistence.db.SupportSQLiteStatement; 20 | import android.database.sqlite.SQLiteStatement; 21 | 22 | /** 23 | * Delegates all calls to a {@link SQLiteStatement}. 24 | */ 25 | class FrameworkSQLiteStatement implements SupportSQLiteStatement { 26 | private final SQLiteStatement mDelegate; 27 | 28 | /** 29 | * Creates a wrapper around a framework {@link SQLiteStatement}. 30 | * 31 | * @param delegate The SQLiteStatement to delegate calls to. 32 | */ 33 | @SuppressWarnings("WeakerAccess") 34 | public FrameworkSQLiteStatement(SQLiteStatement delegate) { 35 | mDelegate = delegate; 36 | } 37 | 38 | @Override 39 | public void bindNull(int index) { 40 | mDelegate.bindNull(index); 41 | } 42 | 43 | @Override 44 | public void bindLong(int index, long value) { 45 | mDelegate.bindLong(index, value); 46 | } 47 | 48 | @Override 49 | public void bindDouble(int index, double value) { 50 | mDelegate.bindDouble(index, value); 51 | } 52 | 53 | @Override 54 | public void bindString(int index, String value) { 55 | mDelegate.bindString(index, value); 56 | } 57 | 58 | @Override 59 | public void bindBlob(int index, byte[] value) { 60 | mDelegate.bindBlob(index, value); 61 | } 62 | 63 | @Override 64 | public void clearBindings() { 65 | mDelegate.clearBindings(); 66 | } 67 | 68 | @Override 69 | public void execute() { 70 | mDelegate.execute(); 71 | } 72 | 73 | @Override 74 | public int executeUpdateDelete() { 75 | return mDelegate.executeUpdateDelete(); 76 | } 77 | 78 | @Override 79 | public long executeInsert() { 80 | return mDelegate.executeInsert(); 81 | } 82 | 83 | @Override 84 | public long simpleQueryForLong() { 85 | return mDelegate.simpleQueryForLong(); 86 | } 87 | 88 | @Override 89 | public String simpleQueryForString() { 90 | return mDelegate.simpleQueryForString(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alberto Giunta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SQLiteAssetHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 readyState Software Ltd, 2007 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 18 | 19 | import android.content.Context; 20 | import android.database.DatabaseErrorHandler; 21 | import android.database.sqlite.SQLiteDatabase; 22 | import android.database.sqlite.SQLiteDatabase.CursorFactory; 23 | import android.database.sqlite.SQLiteException; 24 | import android.database.sqlite.SQLiteOpenHelper; 25 | import android.util.Log; 26 | 27 | import java.io.File; 28 | import java.io.FileOutputStream; 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.util.ArrayList; 32 | import java.util.Collections; 33 | import java.util.List; 34 | import java.util.zip.ZipInputStream; 35 | 36 | /** 37 | * A helper class to manage database creation and version management using 38 | * an application's raw asset files. 39 | * 40 | * This class provides developers with a simple way to ship their Android app 41 | * with an existing SQLite database (which may be pre-populated with data) and 42 | * to manage its initial creation and any upgrades required with subsequent 43 | * version releases. 44 | * 45 | *

This class makes it easy for {@link android.content.ContentProvider} 46 | * implementations to defer opening and upgrading the database until first use, 47 | * to avoid blocking application startup with long-running database upgrades. 48 | * 49 | *

For examples see 50 | * https://github.com/jgilfelt/android-sqlite-asset-helper 51 | * 52 | *

Note: this class assumes 53 | * monotonically increasing version numbers for upgrades. Also, there 54 | * is no concept of a database downgrade; installing a new version of 55 | * your app which uses a lower version number than a 56 | * previously-installed version will result in undefined behavior.

57 | */ 58 | public class SQLiteAssetHelper extends SQLiteOpenHelper { 59 | 60 | private static final String TAG = SQLiteAssetHelper.class.getSimpleName(); 61 | private static final String ASSET_DB_PATH = "databases"; 62 | 63 | private final Context mContext; 64 | private final String mName; 65 | private final CursorFactory mFactory; 66 | private final int mNewVersion; 67 | 68 | private SQLiteDatabase mDatabase = null; 69 | private boolean mIsInitializing = false; 70 | 71 | private String mDatabasePath; 72 | 73 | private String mAssetPath; 74 | 75 | private String mUpgradePathFormat; 76 | 77 | private int mForcedUpgradeVersion = 0; 78 | 79 | /** 80 | * Create a helper object to create, open, and/or manage a database in 81 | * a specified location. 82 | * This method always returns very quickly. The database is not actually 83 | * created or opened until one of {@link #getWritableDatabase} or 84 | * {@link #getReadableDatabase} is called. 85 | * 86 | * @param context to use to open or create the database 87 | * @param name of the database file 88 | * @param storageDirectory to store the database file upon creation; caller must 89 | * ensure that the specified absolute path is available and can be written to 90 | * @param factory to use for creating cursor objects, or null for the default 91 | * @param version number of the database (starting at 1); if the database is older, 92 | * SQL file(s) contained within the application assets folder will be used to 93 | * upgrade the database 94 | */ 95 | public SQLiteAssetHelper(Context context, String name, String storageDirectory, CursorFactory factory, int version, DatabaseErrorHandler errorHandler) { 96 | super(context, name, factory, version, errorHandler); 97 | 98 | if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); 99 | if (name == null) throw new IllegalArgumentException("Database name cannot be null"); 100 | 101 | mContext = context; 102 | mName = name; 103 | mFactory = factory; 104 | mNewVersion = version; 105 | 106 | mAssetPath = ASSET_DB_PATH + "/" + name; 107 | if (storageDirectory != null) { 108 | mDatabasePath = storageDirectory; 109 | } else { 110 | mDatabasePath = context.getApplicationInfo().dataDir + "/databases"; 111 | } 112 | mUpgradePathFormat = ASSET_DB_PATH + "/" + name + "_upgrade_%s-%s.sql"; 113 | } 114 | 115 | /** 116 | * Create a helper object to create, open, and/or manage a database in 117 | * the application's default private data directory. 118 | * This method always returns very quickly. The database is not actually 119 | * created or opened until one of {@link #getWritableDatabase} or 120 | * {@link #getReadableDatabase} is called. 121 | * 122 | * @param context to use to open or create the database 123 | * @param name of the database file 124 | * @param factory to use for creating cursor objects, or null for the default 125 | * @param version number of the database (starting at 1); if the database is older, 126 | * SQL file(s) contained within the application assets folder will be used to 127 | * upgrade the database 128 | */ 129 | public SQLiteAssetHelper(Context context, String name, CursorFactory factory, int version) { 130 | this(context, name, null, factory, version, null); 131 | } 132 | 133 | /** 134 | * Create and/or open a database that will be used for reading and writing. 135 | * The first time this is called, the database will be extracted and copied 136 | * from the application's assets folder. 137 | * 138 | *

Once opened successfully, the database is cached, so you can 139 | * call this method every time you need to write to the database. 140 | * (Make sure to call {@link #close} when you no longer need the database.) 141 | * Errors such as bad permissions or a full disk may cause this method 142 | * to fail, but future attempts may succeed if the problem is fixed.

143 | * 144 | *

Database upgrade may take a long time, you 145 | * should not call this method from the application main thread, including 146 | * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. 147 | * 148 | * @return a read/write database object valid until {@link #close} is called 149 | * @throws SQLiteException if the database cannot be opened for writing 150 | */ 151 | @Override 152 | public synchronized SQLiteDatabase getWritableDatabase() { 153 | if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { 154 | return mDatabase; // The database is already open for business 155 | } 156 | 157 | if (mIsInitializing) { 158 | throw new IllegalStateException("getWritableDatabase called recursively"); 159 | } 160 | 161 | // If we have a read-only database open, someone could be using it 162 | // (though they shouldn't), which would cause a lock to be held on 163 | // the file, and our attempts to open the database read-write would 164 | // fail waiting for the file lock. To prevent that, we acquire the 165 | // lock on the read-only database, which shuts out other users. 166 | 167 | boolean success = false; 168 | SQLiteDatabase db = null; 169 | //if (mDatabase != null) mDatabase.lock(); 170 | try { 171 | mIsInitializing = true; 172 | //if (mName == null) { 173 | // db = SQLiteDatabase.create(null); 174 | //} else { 175 | // db = mContext.openOrCreateDatabase(mName, 0, mFactory); 176 | //} 177 | db = createOrOpenDatabase(false); 178 | 179 | int version = db.getVersion(); 180 | 181 | // do force upgrade 182 | if (version != 0 && version < mForcedUpgradeVersion) { 183 | db = createOrOpenDatabase(true); 184 | db.setVersion(mNewVersion); 185 | version = db.getVersion(); 186 | } 187 | 188 | if (version != mNewVersion) { 189 | db.beginTransaction(); 190 | try { 191 | if (version == 0) { 192 | onCreate(db); 193 | } else { 194 | if (version > mNewVersion) { 195 | Log.w(TAG, "Can't downgrade read-only database from version " + 196 | version + " to " + mNewVersion + ": " + db.getPath()); 197 | } 198 | onUpgrade(db, version, mNewVersion); 199 | } 200 | db.setVersion(mNewVersion); 201 | db.setTransactionSuccessful(); 202 | } finally { 203 | db.endTransaction(); 204 | } 205 | } 206 | 207 | onOpen(db); 208 | success = true; 209 | return db; 210 | } finally { 211 | mIsInitializing = false; 212 | if (success) { 213 | if (mDatabase != null) { 214 | try { 215 | mDatabase.close(); 216 | } catch (Exception e) { 217 | } 218 | //mDatabase.unlock(); 219 | } 220 | mDatabase = db; 221 | } else { 222 | //if (mDatabase != null) mDatabase.unlock(); 223 | if (db != null) db.close(); 224 | } 225 | } 226 | 227 | } 228 | 229 | /** 230 | * Create and/or open a database. This will be the same object returned by 231 | * {@link #getWritableDatabase} unless some problem, such as a full disk, 232 | * requires the database to be opened read-only. In that case, a read-only 233 | * database object will be returned. If the problem is fixed, a future call 234 | * to {@link #getWritableDatabase} may succeed, in which case the read-only 235 | * database object will be closed and the read/write object will be returned 236 | * in the future. 237 | * 238 | *

Like {@link #getWritableDatabase}, this method may 239 | * take a long time to return, so you should not call it from the 240 | * application main thread, including from 241 | * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. 242 | * 243 | * @return a database object valid until {@link #getWritableDatabase} 244 | * or {@link #close} is called. 245 | * @throws SQLiteException if the database cannot be opened 246 | */ 247 | @Override 248 | public synchronized SQLiteDatabase getReadableDatabase() { 249 | if (mDatabase != null && mDatabase.isOpen()) { 250 | return mDatabase; // The database is already open for business 251 | } 252 | 253 | if (mIsInitializing) { 254 | throw new IllegalStateException("getReadableDatabase called recursively"); 255 | } 256 | 257 | try { 258 | return getWritableDatabase(); 259 | } catch (SQLiteException e) { 260 | if (mName == null) throw e; // Can't open a temp database read-only! 261 | Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e); 262 | } 263 | 264 | SQLiteDatabase db = null; 265 | try { 266 | mIsInitializing = true; 267 | String path = mContext.getDatabasePath(mName).getPath(); 268 | db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY); 269 | if (db.getVersion() != mNewVersion) { 270 | throw new SQLiteException("Can't upgrade read-only database from version " + 271 | db.getVersion() + " to " + mNewVersion + ": " + path); 272 | } 273 | 274 | onOpen(db); 275 | Log.w(TAG, "Opened " + mName + " in read-only mode"); 276 | mDatabase = db; 277 | return mDatabase; 278 | } finally { 279 | mIsInitializing = false; 280 | if (db != null && db != mDatabase) db.close(); 281 | } 282 | } 283 | 284 | /** 285 | * Close any open database object. 286 | */ 287 | @Override 288 | public synchronized void close() { 289 | if (mIsInitializing) throw new IllegalStateException("Closed during initialization"); 290 | 291 | if (mDatabase != null && mDatabase.isOpen()) { 292 | mDatabase.close(); 293 | mDatabase = null; 294 | } 295 | } 296 | 297 | @Override 298 | public void onConfigure(SQLiteDatabase db) { 299 | // not supported! 300 | } 301 | 302 | @Override 303 | public void onCreate(SQLiteDatabase db) { 304 | // do nothing - createOrOpenDatabase() is called in 305 | // getWritableDatabase() to handle database creation. 306 | } 307 | 308 | @Override 309 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 310 | 311 | Log.w(TAG, "Upgrading database " + mName + " from version " + oldVersion + " to " + newVersion + "..."); 312 | 313 | ArrayList paths = new ArrayList(); 314 | getUpgradeFilePaths(oldVersion, newVersion - 1, newVersion, paths); 315 | 316 | if (paths.isEmpty()) { 317 | Log.e(TAG, "no upgrade script path from " + oldVersion + " to " + newVersion); 318 | throw new SQLiteAssetException("no upgrade script path from " + oldVersion + " to " + newVersion); 319 | } 320 | 321 | Collections.sort(paths, new VersionComparator()); 322 | for (String path : paths) { 323 | try { 324 | Log.w(TAG, "processing upgrade: " + path); 325 | InputStream is = mContext.getAssets().open(path); 326 | String sql = com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.Utils.convertStreamToString(is); 327 | if (sql != null) { 328 | List cmds = com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.Utils.splitSqlScript(sql, ';'); 329 | for (String cmd : cmds) { 330 | if (cmd.trim().length() > 0) { 331 | db.execSQL(cmd); 332 | } 333 | } 334 | } 335 | } catch (IOException e) { 336 | e.printStackTrace(); 337 | } 338 | } 339 | 340 | Log.w(TAG, "Successfully upgraded database " + mName + " from version " + oldVersion + " to " + newVersion); 341 | 342 | } 343 | 344 | @Override 345 | public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 346 | // not supported! 347 | } 348 | 349 | /** 350 | * Bypass the upgrade process (for each increment up to a given version) and simply 351 | * overwrite the existing database with the supplied asset file. 352 | * 353 | * @param version bypass upgrade up to this version number - should never be greater than the 354 | * latest database version. 355 | * @deprecated use {@link #setForcedUpgrade} instead. 356 | */ 357 | @Deprecated 358 | public void setForcedUpgradeVersion(int version) { 359 | setForcedUpgrade(version); 360 | } 361 | 362 | /** 363 | * Bypass the upgrade process (for each increment up to a given version) and simply 364 | * overwrite the existing database with the supplied asset file. 365 | * 366 | * @param version bypass upgrade up to this version number - should never be greater than the 367 | * latest database version. 368 | */ 369 | public void setForcedUpgrade(int version) { 370 | mForcedUpgradeVersion = version; 371 | } 372 | 373 | /** 374 | * Bypass the upgrade process for every version increment and simply overwrite the existing 375 | * database with the supplied asset file. 376 | */ 377 | public void setForcedUpgrade() { 378 | setForcedUpgrade(mNewVersion); 379 | } 380 | 381 | private SQLiteDatabase createOrOpenDatabase(boolean force) throws SQLiteAssetException { 382 | 383 | // test for the existence of the db file first and don't attempt open 384 | // to prevent the error trace in log on API 14+ 385 | SQLiteDatabase db = null; 386 | File file = new File(mDatabasePath + "/" + mName); 387 | if (file.exists()) { 388 | db = returnDatabase(); 389 | } 390 | //SQLiteDatabase db = returnDatabase(); 391 | 392 | if (db != null) { 393 | // database already exists 394 | if (force) { 395 | Log.w(TAG, "forcing database upgrade!"); 396 | copyDatabaseFromAssets(); 397 | db = returnDatabase(); 398 | } 399 | return db; 400 | } else { 401 | // database does not exist, copy it from assets and return it 402 | copyDatabaseFromAssets(); 403 | db = returnDatabase(); 404 | return db; 405 | } 406 | } 407 | 408 | private SQLiteDatabase returnDatabase() { 409 | try { 410 | SQLiteDatabase db = SQLiteDatabase.openDatabase(mDatabasePath + "/" + mName, mFactory, SQLiteDatabase.OPEN_READWRITE); 411 | Log.i(TAG, "successfully opened database " + mName); 412 | return db; 413 | } catch (SQLiteException e) { 414 | Log.w(TAG, "could not open database " + mName + " - " + e.getMessage()); 415 | return null; 416 | } 417 | } 418 | 419 | private void copyDatabaseFromAssets() throws SQLiteAssetException { 420 | Log.w(TAG, "copying database from assets..."); 421 | 422 | String path = mAssetPath; 423 | String dest = mDatabasePath + "/" + mName; 424 | InputStream is; 425 | boolean isZip = false; 426 | 427 | try { 428 | // try uncompressed 429 | is = mContext.getAssets().open(path); 430 | } catch (IOException e) { 431 | // try zip 432 | try { 433 | is = mContext.getAssets().open(path + ".zip"); 434 | isZip = true; 435 | } catch (IOException e2) { 436 | // try gzip 437 | try { 438 | is = mContext.getAssets().open(path + ".gz"); 439 | } catch (IOException e3) { 440 | SQLiteAssetException se = new SQLiteAssetException("Missing " + mAssetPath + " file (or .zip, .gz archive) in assets, or target folder not writable"); 441 | se.setStackTrace(e3.getStackTrace()); 442 | throw se; 443 | } 444 | } 445 | } 446 | 447 | try { 448 | File f = new File(mDatabasePath + "/"); 449 | if (!f.exists()) { 450 | f.mkdir(); 451 | } 452 | if (isZip) { 453 | ZipInputStream zis = com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.Utils.getFileFromZip(is); 454 | if (zis == null) { 455 | throw new SQLiteAssetException("Archive is missing a SQLite database file"); 456 | } 457 | com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.Utils.writeExtractedFileToDisk(zis, new FileOutputStream(dest)); 458 | } else { 459 | com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.Utils.writeExtractedFileToDisk(is, new FileOutputStream(dest)); 460 | } 461 | 462 | Log.w(TAG, "database copy complete"); 463 | 464 | } catch (IOException e) { 465 | SQLiteAssetException se = new SQLiteAssetException("Unable to write " + dest + " to data directory"); 466 | se.setStackTrace(e.getStackTrace()); 467 | throw se; 468 | } 469 | } 470 | 471 | private InputStream getUpgradeSQLStream(int oldVersion, int newVersion) { 472 | String path = String.format(mUpgradePathFormat, oldVersion, newVersion); 473 | try { 474 | return mContext.getAssets().open(path); 475 | } catch (IOException e) { 476 | Log.w(TAG, "missing database upgrade script: " + path); 477 | return null; 478 | } 479 | } 480 | 481 | private void getUpgradeFilePaths(int baseVersion, int start, int end, ArrayList paths) { 482 | 483 | int a; 484 | int b; 485 | 486 | InputStream is = getUpgradeSQLStream(start, end); 487 | if (is != null) { 488 | String path = String.format(mUpgradePathFormat, start, end); 489 | paths.add(path); 490 | a = start - 1; 491 | b = start; 492 | is = null; 493 | } else { 494 | a = start - 1; 495 | b = end; 496 | } 497 | 498 | if (a < baseVersion) { 499 | return; 500 | } else { 501 | getUpgradeFilePaths(baseVersion, a, b, paths); // recursive call 502 | } 503 | 504 | } 505 | 506 | /** 507 | * An exception that indicates there was an error with SQLite asset retrieval or parsing. 508 | */ 509 | @SuppressWarnings("serial") 510 | public static class SQLiteAssetException extends SQLiteException { 511 | 512 | public SQLiteAssetException() { 513 | } 514 | 515 | public SQLiteAssetException(String error) { 516 | super(error); 517 | } 518 | } 519 | 520 | } 521 | -------------------------------------------------------------------------------- /Utils.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Scanner; 11 | import java.util.zip.ZipEntry; 12 | import java.util.zip.ZipInputStream; 13 | 14 | class Utils { 15 | 16 | private static final String TAG = SQLiteAssetHelper.class.getSimpleName(); 17 | 18 | public static List splitSqlScript(String script, char delim) { 19 | List statements = new ArrayList(); 20 | StringBuilder sb = new StringBuilder(); 21 | boolean inLiteral = false; 22 | char[] content = script.toCharArray(); 23 | for (int i = 0; i < script.length(); i++) { 24 | if (content[i] == '"') { 25 | inLiteral = !inLiteral; 26 | } 27 | if (content[i] == delim && !inLiteral) { 28 | if (sb.length() > 0) { 29 | statements.add(sb.toString().trim()); 30 | sb = new StringBuilder(); 31 | } 32 | } else { 33 | sb.append(content[i]); 34 | } 35 | } 36 | if (sb.length() > 0) { 37 | statements.add(sb.toString().trim()); 38 | } 39 | return statements; 40 | } 41 | 42 | public static void writeExtractedFileToDisk(InputStream in, OutputStream outs) throws IOException { 43 | byte[] buffer = new byte[1024]; 44 | int length; 45 | while ((length = in.read(buffer)) > 0) { 46 | outs.write(buffer, 0, length); 47 | } 48 | outs.flush(); 49 | outs.close(); 50 | in.close(); 51 | } 52 | 53 | public static ZipInputStream getFileFromZip(InputStream zipFileStream) throws IOException { 54 | ZipInputStream zis = new ZipInputStream(zipFileStream); 55 | ZipEntry ze; 56 | while ((ze = zis.getNextEntry()) != null) { 57 | Log.w(TAG, "extracting file: '" + ze.getName() + "'..."); 58 | return zis; 59 | } 60 | return null; 61 | } 62 | 63 | public static String convertStreamToString(InputStream is) { 64 | return new Scanner(is).useDelimiter("\\A").next(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /VersionComparator.java: -------------------------------------------------------------------------------- 1 | package com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset; 2 | 3 | import android.util.Log; 4 | 5 | import com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.SQLiteAssetHelper.SQLiteAssetException; 6 | 7 | import java.util.Comparator; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * Compare paths by their upgrade version numbers, instead of using 13 | * alphanumeric comparison on plain file names. This prevents the upgrade 14 | * scripts from being applied out of order when they first move to double-, 15 | * triple-, etc. digits. 16 | *

17 | * For example, this fixes an upgrade that would apply 2 different upgrade 18 | * files from version 9 to 11 (..._updated_9_10 and 19 | * ..._updated_10_11) from using the incorrect 20 | * alphanumeric order of 10_11 before 9_10. 21 | *

22 | */ 23 | class VersionComparator implements Comparator { 24 | 25 | private static final String TAG = com.jaus.albertogiunta.justintrain_oraritreni.db.sqliteAsset.SQLiteAssetHelper.class.getSimpleName(); 26 | 27 | private Pattern pattern = Pattern 28 | .compile(".*_upgrade_([0-9]+)-([0-9]+).*"); 29 | 30 | /** 31 | * Compares the two specified upgrade script strings to determine their 32 | * relative ordering considering their two version numbers. Assumes all 33 | * database names used are the same, as this function only compares the 34 | * two version numbers. 35 | * 36 | * @param file0 an upgrade script file name 37 | * @param file1 a second upgrade script file name to compare with file0 38 | * @return an integer < 0 if file0 should be applied before file1, 0 if 39 | * they are equal (though that shouldn't happen), and > 0 if 40 | * file0 should be applied after file1. 41 | * @throws SQLiteAssetException thrown if the strings are not in the correct upgrade 42 | * script format of: 43 | * databasename_fromVersionInteger_toVersionInteger 44 | */ 45 | @Override 46 | public int compare(String file0, String file1) { 47 | Matcher m0 = pattern.matcher(file0); 48 | Matcher m1 = pattern.matcher(file1); 49 | 50 | if (!m0.matches()) { 51 | Log.w(TAG, "could not parse upgrade script file: " + file0); 52 | throw new SQLiteAssetException("Invalid upgrade script file"); 53 | } 54 | 55 | if (!m1.matches()) { 56 | Log.w(TAG, "could not parse upgrade script file: " + file1); 57 | throw new SQLiteAssetException("Invalid upgrade script file"); 58 | } 59 | 60 | int v0_from = Integer.valueOf(m0.group(1)); 61 | int v1_from = Integer.valueOf(m1.group(1)); 62 | int v0_to = Integer.valueOf(m0.group(2)); 63 | int v1_to = Integer.valueOf(m1.group(2)); 64 | 65 | if (v0_from == v1_from) { 66 | // 'from' versions match for both; check 'to' version next 67 | 68 | if (v0_to == v1_to) { 69 | return 0; 70 | } 71 | 72 | return v0_to < v1_to ? -1 : 1; 73 | } 74 | 75 | return v0_from < v1_from ? -1 : 1; 76 | } 77 | } 78 | --------------------------------------------------------------------------------