Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SQLiteOpenHelper problem with fully qualified DB path name

Tags:

In my app, I use...

myFilesDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()                       + "/Android/data/" + packageName + "/files"); myFilesDir.mkdirs(); 

This is fine and the resulting path is...

/mnt/sdcard/Android/data/com.mycompany.myApp/files 

I need a SQLite DB which I want to store on the SD card so I extend SQLiteOpenHelper as follows...

public class myDbHelper extends SQLiteOpenHelper {      public myDbHelper(Context context, String name, CursorFactory factory, int version) {         // NOTE I prefix the full path of my files directory to 'name'         super(context, myFilesDir + "/" + name, factory, version);     }      @Override     public void onCreate(SQLiteDatabase db) {         // Create tables and populate with default data...     } } 

So far so good - the first time I call getReadableDatabase() or getWriteableDatabase() the empty DB is created on the SD card and onCreate() populates it.

So here's the problem - the app is in beta testing with maybe 5 or 6 people and, like me, they're running Android v2.2 and everything works fine. I have one tester, however, running v2.1 and when myDbHelper tries to create the DB on first use, it crashes with the following...

E/AndroidRuntime( 3941): Caused by: java.lang.IllegalArgumentException: File /nand/Android/data/com.mycompany.myApp/files/myApp-DB.db3 contains a path separator E/AndroidRuntime( 3941): at android.app.ApplicationContext.makeFilename(ApplicationContext.java:1445) E/AndroidRuntime( 3941): at android.app.ApplicationContext.openOrCreateDatabase(ApplicationContext.java:473) E/AndroidRuntime( 3941): at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:193) E/AndroidRuntime( 3941): at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:98) E/AndroidRuntime( 3941): at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:158) 

The path for the files directory is an odd one ("/nand") as it's internal memory although not the phone's own internal memory - but it is the path returned by getExternalStorageDirectory() for this device.

I can see three possible answers...

  1. Although acceptable on v2.2, specifying a fully qualified path for DB name isn't recommended and will fail on earlier versions
  2. Fully qualified paths are acceptable for SD card storage but the "/nand" path is being interpreted as 'internal' and only relative paths are acceptable in this case
  3. Something else which I'm missing completely

If any or all of the above apply I'd appreciate it if somebody could help with how I should approach this.

Thanks.

like image 431
Squonk Avatar asked Mar 16 '11 21:03

Squonk


People also ask

What is difference SQLiteOpenHelper and SQLiteDatabase?

there is no difference between SQLiteOpenHeloper::close() and SQLiteDatabase::close() . SQLiteOpenHeloper::close() is just a wrapper around SQLiteDatabase::close() . but as a rule of thumb, either let SQLiteOpenHelper manages your connections, or don't use it and manage it yourself. see this blog post.

What is the use of onUpgrade function in SQLiteOpenHelper?

onUpgrade. Called when the database needs to be upgraded. The implementation should use this method to drop tables, add tables, or do anything else it needs to upgrade to the new schema version. The SQLite ALTER TABLE documentation can be found here.

What is the use of SQLiteOpenHelper class?

SQLiteOpenHelper class is used for database creation and version management. For performing any database operation, you have to provide the implementation of onCreate() and onUpgrade() methods of SQLiteOpenHelper class.


2 Answers

You can use the SQLiteOpenHelper with a custom path if you provide a custom ContextClass and if you have write access in the target directory.

public class DatabaseHelper extends SQLiteOpenHelper {   private static final int DATABASE_VERSION = 3;     .....    DatabaseHelper(final Context context, String databaseName)  {     super(new DatabaseContext(context), databaseName, null, DATABASE_VERSION);   } } 

And here is the custom DatabaseContext class that does all the magic:

class DatabaseContext extends ContextWrapper {    private static final String DEBUG_CONTEXT = "DatabaseContext";    public DatabaseContext(Context base) {     super(base);   }    @Override   public File getDatabasePath(String name)  {     File sdcard = Environment.getExternalStorageDirectory();         String dbfile = sdcard.getAbsolutePath() + File.separator+ "databases" + File.separator + name;     if (!dbfile.endsWith(".db")) {       dbfile += ".db" ;     }      File result = new File(dbfile);      if (!result.getParentFile().exists()) {       result.getParentFile().mkdirs();     }      if (Log.isLoggable(DEBUG_CONTEXT, Log.WARN)) {       Log.w(DEBUG_CONTEXT, "getDatabasePath(" + name + ") = " + result.getAbsolutePath());     }      return result;   }    /* this version is called for android devices >= api-11. thank to @damccull for fixing this. */   @Override   public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {     return openOrCreateDatabase(name,mode, factory);   }    /* this version is called for android devices < api-11 */   @Override   public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) {     SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);     // SQLiteDatabase result = super.openOrCreateDatabase(name, mode, factory);     if (Log.isLoggable(DEBUG_CONTEXT, Log.WARN)) {       Log.w(DEBUG_CONTEXT, "openOrCreateDatabase(" + name + ",,) = " + result.getPath());     }     return result;   } } 

Update june 2012:
how does this work (@barry question):

Normal android apps have their local database files relative to the app folder. By using a customer context with overwritten getDatabasePath() the database is now relative to a different directory on the sd card.

Update feb 2015:
After replacing my old android-2.2 device with a new android-4.4 device I found out that my solution didn't work anymore. Thanks to @damccull-s answer I was able to fix it. I have updated this answer so this should be a working example again.

Update may 2017:

Statistics: This aproach is used in more than 200 github projects

like image 195
k3b Avatar answered Sep 22 '22 07:09

k3b


Historically, you have not been able to use paths with SQLiteOpenHelper. It only worked on simple filenames. I had not realized that they relaxed that restriction in Android 2.2.

If you wish to use databases on the SD card, and you wish to support Android 2.1 and earlier, you cannot use SQLiteOpenHelper.

Sorry!

like image 35
CommonsWare Avatar answered Sep 20 '22 07:09

CommonsWare