SQLiteAssetPortal.java

  1. package net.zer0bandwidth.android.lib.database;

  2. import android.content.Context;
  3. import android.database.sqlite.SQLiteDatabase;
  4. import android.database.sqlite.SQLiteOpenHelper ;
  5. import android.util.Log;

  6. import java.io.File;
  7. import java.io.FileOutputStream;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import java.io.OutputStream;

  11. /**
  12.  * Provides a {@link SQLiteOpenHelper} implementation which manages a read-only
  13.  * database that is loaded from an asset file.
  14.  *
  15.  * <p>See <a href="https://blog.reigndesign.com/blog/using-your-own-sqlite-database-in-android-applications/">
  16.  * Using Your Own SQLite Database in Android Applications</a> for the basis of
  17.  * this implementation.</p>
  18.  *
  19.  * <h4>Example</h4>
  20.  *
  21.  * <pre>
  22.  * public class MyDB extends SQLiteAssetPortal
  23.  * {
  24.  *     protected static final String DB_NAME = "mydb" ;
  25.  *
  26.  *     /**
  27.  *      * Rather than using this to represent the version of the database
  28.  *      * schema, use this constant to represent the version of the asset. Thus,
  29.  *      * whenever new content is added to the asset, the app will know to
  30.  *      * overwrite its database with the contents of the new asset. See the
  31.  *      * example of onUpgrade() below.
  32.  *     {@literal *}/
  33.  *     protected static final int DB_VERSION = 2 ;
  34.  *
  35.  *     /** Filename of the asset containing the static database instance. {@literal *}/
  36.  *     protected static final String DB_SOURCE_ASSET = "mydb.v2.db" ;
  37.  *
  38.  *     public MyDB( Context ctx )
  39.  *     { super( ctx, DB_NAME, null, DB_VERSION ) ; }
  40.  *
  41.  *    {@literal @}Override
  42.  *     public abstract String getAssetName()
  43.  *     { return DB_SOURCE_ASSET ; }
  44.  * }
  45.  * </pre>
  46.  *
  47.  * @since zer0bandwidth-net/android 0.1.4 (#34)
  48.  */
  49. public abstract class SQLiteAssetPortal
  50. extends SQLitePortal
  51. {
  52. public static final String LOG_TAG =
  53. SQLiteAssetPortal.class.getSimpleName() ;

  54. /// Inner Classes //////////////////////////////////////////////////////////////

  55. /**
  56.  * Allows the {@link SQLiteAssetPortal} to create a persistent connection to
  57.  * its underlying database on a background thread.
  58.  * @since zer0bandwidth-net/android 0.1.4 (#34)
  59.  */
  60. protected class ConnectionTask
  61. extends SQLitePortal.ConnectionTask
  62. implements Runnable
  63. {
  64. /**
  65.  * A reference back to the {@link SQLiteAssetPortal} that needs the
  66.  * connection.
  67.  */
  68. protected SQLiteAssetPortal m_dbh = SQLiteAssetPortal.this ;

  69. protected ConnectionTask( ConnectionListener l )
  70. { m_listener = l ; }

  71. @Override
  72. public void run()
  73. {
  74. m_dbh.m_db = null ;
  75. m_dbh.m_bIsConnected = false ;
  76. try { m_dbh.m_db = m_dbh.getReadableDatabase() ; }
  77. catch( Exception x )
  78. { Log.e( LOG_TAG, "Could not establish initial connection." ) ; }
  79. if( m_dbh.m_bNeedsCopy ) // set by onCreate() / onUpgrade()
  80. { // Close the database connection, copy from asset, and reopen DB.
  81. m_dbh.close() ;
  82. m_dbh.copyFromAsset() ;
  83. try
  84. {
  85. m_dbh.m_db = m_dbh.getReadableDatabase() ;
  86. // Explicitly override the copy flag after this second call,
  87. // to avoid spurious re-copying of the DB. This must be done
  88. // because onCreate() can still be called by Android as part
  89. // of the SQLite DB connection life cycle, and our override
  90. // would blindly set the copy flag to true.
  91. m_dbh.m_bNeedsCopy = false ;
  92. }
  93. catch( Exception x )
  94. {
  95. Log.e( LOG_TAG,
  96. "Could not connect after copying from asset." ) ;
  97. }
  98. }
  99. m_dbh.m_bIsConnected = ( m_dbh.m_db != null ) ;
  100. if( m_dbh.m_bIsConnected && m_listener != null )
  101. m_listener.onDatabaseConnected(m_dbh) ;
  102. }
  103. }

  104. /// Instance Members ///////////////////////////////////////////////////////////

  105. /**
  106.  * The only method in which the old and new schema versions are exposed is
  107.  * in {@link #onUpgrade}; thus, that method will set this indicator flag
  108.  * so that the consumer can then explicitly invoke {@link #copyFromAsset}.
  109.  */
  110. protected boolean m_bNeedsCopy = false ;

  111. /// Inherited Constructors (must duplicate here for descendants) ///////////////

  112. public SQLiteAssetPortal( Context ctx, String sDatabaseName,
  113.   SQLiteDatabase.CursorFactory cf, int nVersion )
  114. { super( ctx, sDatabaseName, cf, nVersion ) ; }

  115. /// android.database.sqlite.SQLiteOpenHelper ///////////////////////////////////

  116. /**
  117.  * If the database did not previously exist, then we need to copy it from
  118.  * the asset.
  119.  *
  120.  * <p>Note that, since {@link ConnectionTask} executes
  121.  * {@link SQLiteOpenHelper#getReadableDatabase()} twice, it is possible that
  122.  * this method will be called spuriously during the second invocation.
  123.  * However, since the task immediately resets {@link #m_bNeedsCopy} to
  124.  * {@code false} after this, this has no negative consequence, other than
  125.  * the time wasted on the method call.</p>
  126.  */
  127. @Override
  128. public final void onCreate( SQLiteDatabase db )
  129. {
  130. Log.d( LOG_TAG, "onCreate() called; asset may be copied." ) ;
  131. m_bNeedsCopy = true ;
  132. }

  133. /**
  134.  * Copy the database asset only if an upgrade is needed.
  135.  */
  136. @Override
  137. public final void onUpgrade( SQLiteDatabase db, int nOld, int nNew )
  138. {
  139. m_bNeedsCopy = ( nOld < nNew ) ;
  140. if( m_bNeedsCopy )
  141. {
  142. Log.d( LOG_TAG, (new StringBuilder())
  143. .append( "onUpdate(): Asset should be copied; old version [" )
  144. .append( nOld ).append( "] is less than new version [" )
  145. .append( nNew ).append( "]." )
  146. .toString()
  147. );
  148. }
  149. }

  150. /// net.zer0bandwidth.android.lib.database.SQLitePortal ////////////////////////

  151. /**
  152.  * Forces the connection to be read-only, and uses the descendant version of
  153.  * {@link ConnectionTask} to open, and then copy, the database.
  154.  * @param bReadOnly placebo - always overridden as {@code true} in this
  155.  *  version of the method
  156.  * @param l the listener for the "on connected" callback
  157.  * @return (fluid)
  158.  */
  159. @Override
  160. public synchronized SQLiteAssetPortal openDB( boolean bReadOnly, ConnectionListener l )
  161. {
  162. m_bReadOnly = true ;     // Asset-copied databases are always read-only.
  163. (new SQLiteAssetPortal.ConnectionTask(l)).runInBackground() ;
  164. return this ;
  165. }

  166. /// Database from Assets ///////////////////////////////////////////////////////

  167. /**
  168.  * Descendant classes must implement this method to provide the name of the
  169.  * asset from which the database will be replicated.
  170.  * @return the name of the asset which will be copied as the app's database
  171.  */
  172. public abstract String getAssetName() ;

  173. /**
  174.  * If indicated by the flag set during connection, this method overwrites
  175.  * the portal's database with the contents of a static asset packaged in the
  176.  * APK.
  177.  * @return {@code true} if the asset was successfully copied; {@code false}
  178.  *  otherwise
  179.  */
  180. protected boolean copyFromAsset()
  181. {
  182. final String sAssetFileName = this.getAssetName() ;
  183. boolean bSuccess = true ;
  184. InputStream in = null ;
  185. OutputStream out = null ;
  186. String sDatabaseName = this.getDatabaseName() ;
  187. try
  188. {
  189. String sDatabaseFile = this.getPathToDatabaseFile() ;
  190. if( this.databaseExists() )
  191. {
  192. File fOld = new File( sDatabaseFile ) ;
  193. if( fOld.delete() )
  194. Log.i( LOG_TAG, "Deleted previous database file!" ) ;
  195. }
  196. in = m_ctx.getAssets().open( sAssetFileName ) ;
  197. out = new FileOutputStream( sDatabaseFile ) ;
  198. byte[] ayBuffer = new byte[1024] ;
  199. int nLength ;
  200. Log.d( LOG_TAG, (new StringBuilder())
  201. .append( "Copying database from asset [" )
  202. .append( sAssetFileName )
  203. .append( "] to database [" )
  204. .append( sDatabaseName )
  205. .append( "]..." )
  206. .toString()
  207. );
  208. while( ( nLength = in.read(ayBuffer) ) > 0 )
  209. out.write( ayBuffer, 0, nLength ) ;
  210. }
  211. catch( IOException iox )
  212. {
  213. Log.e( LOG_TAG, (new StringBuilder())
  214. .append( "Could not copy asset [" )
  215. .append( sAssetFileName )
  216. .append( "] to database [" )
  217. .append( sDatabaseName )
  218. .append( "]:" )
  219. .toString()
  220. , iox );
  221. bSuccess = false ;
  222. }
  223. finally
  224. { // Ensure the input/output streams are closed.
  225. try { if( in != null ) in.close() ; }
  226. catch( IOException ioxCloseInput )
  227. {
  228. Log.e( LOG_TAG, "Could not close input stream!",
  229. ioxCloseInput ) ;
  230. }
  231. if( out != null )
  232. {
  233. try { out.flush() ;  out.close() ; }
  234. catch( IOException ioxCloseOutput )
  235. {
  236. Log.e( LOG_TAG,
  237. "Could not close output stream!",
  238. ioxCloseOutput
  239. );
  240. }
  241. }
  242. }
  243. return bSuccess ;
  244. }
  245. }