SQLiteAssetPortal.java
package net.zer0bandwidth.android.lib.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper ;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Provides a {@link SQLiteOpenHelper} implementation which manages a read-only
* database that is loaded from an asset file.
*
* <p>See <a href="https://blog.reigndesign.com/blog/using-your-own-sqlite-database-in-android-applications/">
* Using Your Own SQLite Database in Android Applications</a> for the basis of
* this implementation.</p>
*
* <h4>Example</h4>
*
* <pre>
* public class MyDB extends SQLiteAssetPortal
* {
* protected static final String DB_NAME = "mydb" ;
*
* /**
* * Rather than using this to represent the version of the database
* * schema, use this constant to represent the version of the asset. Thus,
* * whenever new content is added to the asset, the app will know to
* * overwrite its database with the contents of the new asset. See the
* * example of onUpgrade() below.
* {@literal *}/
* protected static final int DB_VERSION = 2 ;
*
* /** Filename of the asset containing the static database instance. {@literal *}/
* protected static final String DB_SOURCE_ASSET = "mydb.v2.db" ;
*
* public MyDB( Context ctx )
* { super( ctx, DB_NAME, null, DB_VERSION ) ; }
*
* {@literal @}Override
* public abstract String getAssetName()
* { return DB_SOURCE_ASSET ; }
* }
* </pre>
*
* @since zer0bandwidth-net/android 0.1.4 (#34)
*/
public abstract class SQLiteAssetPortal
extends SQLitePortal
{
public static final String LOG_TAG =
SQLiteAssetPortal.class.getSimpleName() ;
/// Inner Classes //////////////////////////////////////////////////////////////
/**
* Allows the {@link SQLiteAssetPortal} to create a persistent connection to
* its underlying database on a background thread.
* @since zer0bandwidth-net/android 0.1.4 (#34)
*/
protected class ConnectionTask
extends SQLitePortal.ConnectionTask
implements Runnable
{
/**
* A reference back to the {@link SQLiteAssetPortal} that needs the
* connection.
*/
protected SQLiteAssetPortal m_dbh = SQLiteAssetPortal.this ;
protected ConnectionTask( ConnectionListener l )
{ m_listener = l ; }
@Override
public void run()
{
m_dbh.m_db = null ;
m_dbh.m_bIsConnected = false ;
try { m_dbh.m_db = m_dbh.getReadableDatabase() ; }
catch( Exception x )
{ Log.e( LOG_TAG, "Could not establish initial connection." ) ; }
if( m_dbh.m_bNeedsCopy ) // set by onCreate() / onUpgrade()
{ // Close the database connection, copy from asset, and reopen DB.
m_dbh.close() ;
m_dbh.copyFromAsset() ;
try
{
m_dbh.m_db = m_dbh.getReadableDatabase() ;
// Explicitly override the copy flag after this second call,
// to avoid spurious re-copying of the DB. This must be done
// because onCreate() can still be called by Android as part
// of the SQLite DB connection life cycle, and our override
// would blindly set the copy flag to true.
m_dbh.m_bNeedsCopy = false ;
}
catch( Exception x )
{
Log.e( LOG_TAG,
"Could not connect after copying from asset." ) ;
}
}
m_dbh.m_bIsConnected = ( m_dbh.m_db != null ) ;
if( m_dbh.m_bIsConnected && m_listener != null )
m_listener.onDatabaseConnected(m_dbh) ;
}
}
/// Instance Members ///////////////////////////////////////////////////////////
/**
* The only method in which the old and new schema versions are exposed is
* in {@link #onUpgrade}; thus, that method will set this indicator flag
* so that the consumer can then explicitly invoke {@link #copyFromAsset}.
*/
protected boolean m_bNeedsCopy = false ;
/// Inherited Constructors (must duplicate here for descendants) ///////////////
public SQLiteAssetPortal( Context ctx, String sDatabaseName,
SQLiteDatabase.CursorFactory cf, int nVersion )
{ super( ctx, sDatabaseName, cf, nVersion ) ; }
/// android.database.sqlite.SQLiteOpenHelper ///////////////////////////////////
/**
* If the database did not previously exist, then we need to copy it from
* the asset.
*
* <p>Note that, since {@link ConnectionTask} executes
* {@link SQLiteOpenHelper#getReadableDatabase()} twice, it is possible that
* this method will be called spuriously during the second invocation.
* However, since the task immediately resets {@link #m_bNeedsCopy} to
* {@code false} after this, this has no negative consequence, other than
* the time wasted on the method call.</p>
*/
@Override
public final void onCreate( SQLiteDatabase db )
{
Log.d( LOG_TAG, "onCreate() called; asset may be copied." ) ;
m_bNeedsCopy = true ;
}
/**
* Copy the database asset only if an upgrade is needed.
*/
@Override
public final void onUpgrade( SQLiteDatabase db, int nOld, int nNew )
{
m_bNeedsCopy = ( nOld < nNew ) ;
if( m_bNeedsCopy )
{
Log.d( LOG_TAG, (new StringBuilder())
.append( "onUpdate(): Asset should be copied; old version [" )
.append( nOld ).append( "] is less than new version [" )
.append( nNew ).append( "]." )
.toString()
);
}
}
/// net.zer0bandwidth.android.lib.database.SQLitePortal ////////////////////////
/**
* Forces the connection to be read-only, and uses the descendant version of
* {@link ConnectionTask} to open, and then copy, the database.
* @param bReadOnly placebo - always overridden as {@code true} in this
* version of the method
* @param l the listener for the "on connected" callback
* @return (fluid)
*/
@Override
public synchronized SQLiteAssetPortal openDB( boolean bReadOnly, ConnectionListener l )
{
m_bReadOnly = true ; // Asset-copied databases are always read-only.
(new SQLiteAssetPortal.ConnectionTask(l)).runInBackground() ;
return this ;
}
/// Database from Assets ///////////////////////////////////////////////////////
/**
* Descendant classes must implement this method to provide the name of the
* asset from which the database will be replicated.
* @return the name of the asset which will be copied as the app's database
*/
public abstract String getAssetName() ;
/**
* If indicated by the flag set during connection, this method overwrites
* the portal's database with the contents of a static asset packaged in the
* APK.
* @return {@code true} if the asset was successfully copied; {@code false}
* otherwise
*/
protected boolean copyFromAsset()
{
final String sAssetFileName = this.getAssetName() ;
boolean bSuccess = true ;
InputStream in = null ;
OutputStream out = null ;
String sDatabaseName = this.getDatabaseName() ;
try
{
String sDatabaseFile = this.getPathToDatabaseFile() ;
if( this.databaseExists() )
{
File fOld = new File( sDatabaseFile ) ;
if( fOld.delete() )
Log.i( LOG_TAG, "Deleted previous database file!" ) ;
}
in = m_ctx.getAssets().open( sAssetFileName ) ;
out = new FileOutputStream( sDatabaseFile ) ;
byte[] ayBuffer = new byte[1024] ;
int nLength ;
Log.d( LOG_TAG, (new StringBuilder())
.append( "Copying database from asset [" )
.append( sAssetFileName )
.append( "] to database [" )
.append( sDatabaseName )
.append( "]..." )
.toString()
);
while( ( nLength = in.read(ayBuffer) ) > 0 )
out.write( ayBuffer, 0, nLength ) ;
}
catch( IOException iox )
{
Log.e( LOG_TAG, (new StringBuilder())
.append( "Could not copy asset [" )
.append( sAssetFileName )
.append( "] to database [" )
.append( sDatabaseName )
.append( "]:" )
.toString()
, iox );
bSuccess = false ;
}
finally
{ // Ensure the input/output streams are closed.
try { if( in != null ) in.close() ; }
catch( IOException ioxCloseInput )
{
Log.e( LOG_TAG, "Could not close input stream!",
ioxCloseInput ) ;
}
if( out != null )
{
try { out.flush() ; out.close() ; }
catch( IOException ioxCloseOutput )
{
Log.e( LOG_TAG,
"Could not close output stream!",
ioxCloseOutput
);
}
}
}
return bSuccess ;
}
}