Created
October 2, 2018 02:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package pro.tlrs.utils; | |
import android.content.Context; | |
import android.content.pm.PackageInfo; | |
import android.content.pm.PackageManager; | |
import android.content.res.AssetManager; | |
import android.database.DatabaseErrorHandler; | |
import android.database.sqlite.SQLiteDatabase; | |
import android.util.Log; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.lang.ref.WeakReference; | |
import java.util.Arrays; | |
import java.util.concurrent.atomic.AtomicInteger; | |
/** | |
* Provide read-only connection to assets stored database. | |
* | |
* @author thelongrunsmoke. | |
*/ | |
public class SQLiteAssetsHelper { | |
private static final String LOG_TAG = SQLiteAssetsHelper.class.getSimpleName(); | |
private static final String ASSETS_TYPE_FOLDER = "databases"; | |
private static final int MAX_EXTRACTION_TRY_COUNT = 3; | |
private WeakReference<Context> mWeakAppContext; | |
private static AtomicInteger mExtractionTryCount = new AtomicInteger(0); | |
public SQLiteAssetsHelper(Context context) { | |
mWeakAppContext = new WeakReference<>(context.getApplicationContext()); | |
} | |
/** | |
* Open specified database from assets. | |
* | |
* @param name database file name. | |
* @return read-only connection to SQLite database. | |
* @throws DatabaseCorruptionException When stored in assets database file is corrupted. | |
*/ | |
public SQLiteDatabase getReadOnlyDatabase(final String name) throws DatabaseCorruptionException { | |
SQLiteDatabase result = getReadableDatabase(name); | |
if (result == null) throw new DatabaseCorruptionException(name); | |
return result; | |
} | |
/** | |
* Extract specified database from assets and try to open it. | |
* | |
* @param name database file name. | |
* @return read-only connection to SQLite database or null, when can't be extracted or opened a few times. | |
*/ | |
private SQLiteDatabase getReadableDatabase(final String name) { | |
// When too many extraction try, return null. | |
if (mExtractionTryCount.get() >= MAX_EXTRACTION_TRY_COUNT) return null; | |
// Try to extract db. | |
extract(name); | |
// Database error handler. No fair place, but acceptable due low try count. | |
DatabaseErrorHandler handler = new DatabaseErrorHandler() { | |
/** | |
* Execute when database connection can't be establish due file corruption. | |
* @param dbObj database. | |
*/ | |
@Override | |
public void onCorruption(SQLiteDatabase dbObj) { | |
mExtractionTryCount.incrementAndGet(); | |
// Remove bad file, and try again. | |
removeExtracted(name); | |
getReadableDatabase(name); // Warning! Implicit recursion! | |
} | |
}; | |
return SQLiteDatabase.openDatabase(getAssetsPath(ASSETS_TYPE_FOLDER + File.separator + name), null, SQLiteDatabase.OPEN_READONLY, handler); | |
} | |
/** | |
* Extract given database from assets, to app directory on internal storage. | |
* | |
* @param fileName asset name | |
*/ | |
private void extract(final String fileName) { | |
Log.d(LOG_TAG, "extract(): " + fileName); | |
AssetManager assetManager = mWeakAppContext.get().getAssets(); | |
try { | |
String[] files = assetManager.list(ASSETS_TYPE_FOLDER); | |
Log.d(LOG_TAG, "extract(): to extract " + Arrays.toString(files)); | |
for (String file : files) { | |
if (file.endsWith(fileName)) { | |
unpackFromAssets(ASSETS_TYPE_FOLDER + File.separator + file); | |
return; | |
} | |
} | |
} catch (IOException e) { | |
Log.w(LOG_TAG, "extract(): No assets found! Nothing to extract."); | |
} | |
} | |
/** | |
* Extract specified asset to application data folder. | |
* | |
* @param fileName asset file name. | |
*/ | |
private void unpackFromAssets(final String fileName) { | |
Log.d(LOG_TAG, "unpackFromAssets(): prepare to asset extraction " + fileName); | |
// Nothing to do, if already extracted. | |
if (isAssetExtracted(fileName)) return; | |
long start = System.currentTimeMillis(); | |
Log.d(LOG_TAG, "unpackFromAssets(): begin asset extraction " + fileName); | |
File dir = new File(getAssetsPath(null) + File.separator + ASSETS_TYPE_FOLDER); | |
if (!dir.exists()) { | |
if (!dir.mkdirs()) Log.e(LOG_TAG,"unpackFromAssets(): Can't create directory: " + dir); | |
} | |
AssetManager assetManager = mWeakAppContext.get().getAssets(); | |
InputStream in; | |
OutputStream out; | |
// Use stream to extract file. 8kB chank enough in most cases. | |
try { | |
in = assetManager.open(fileName); | |
String newFileName = getAssetsPath(fileName); | |
// Extract to temporary file. It's protect from corrupted extraction. | |
out = new FileOutputStream(newFileName + "_part"); | |
byte[] buffer = new byte[1024 * 8]; | |
int read; | |
while ((read = in.read(buffer)) != -1) { | |
out.write(buffer, 0, read); | |
} | |
in.close(); | |
out.flush(); | |
out.close(); | |
// Rename temporary file to target file. | |
boolean isRenamed = new File(newFileName + "_part").renameTo(new File(newFileName)); | |
Log.d(LOG_TAG, "unpackFromAssets(): Asset extraction complete in " | |
+ Long.toString(System.currentTimeMillis() - start) + "ms. " + | |
"Rename " + (isRenamed ? "to " + newFileName : "failed!")); | |
} catch (Exception e) { | |
Log.e(LOG_TAG, "unpackFromAssets(): Exception: " + e.getMessage()); | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* Obtain path to app data folder on internal storage. | |
* | |
* @return full path. | |
*/ | |
private String getAppDirectory() { | |
String result = ""; | |
PackageManager m = mWeakAppContext.get().getPackageManager(); | |
String s = mWeakAppContext.get().getPackageName(); | |
PackageInfo p; | |
try { | |
p = m.getPackageInfo(s, 0); | |
result = p.applicationInfo.dataDir; | |
} catch (PackageManager.NameNotFoundException e) { | |
Log.w(LOG_TAG, "getAppDirectory(): Package not found"); | |
} | |
return result; | |
} | |
/** | |
* Check asset file existent in app data folder. | |
* | |
* @param fileName asset file name. | |
* @return true if exist. | |
*/ | |
private boolean isAssetExtracted(final String fileName) { | |
File assets = new File(getAssetsPath(fileName)); | |
boolean result = assets.exists(); | |
Log.d(LOG_TAG, "isAssetExtracted(): " + fileName + (result ? " exist." : " not exist.")); | |
return result; | |
} | |
/** | |
* Remove specified file. | |
* | |
* @param fileName asset file name. | |
*/ | |
private void removeExtracted(final String fileName) { | |
File assets = new File(getAssetsPath(fileName)); | |
if (!assets.exists()) return; | |
boolean isDeleted = assets.delete(); | |
if (isDeleted) { | |
Log.d(LOG_TAG, "removeExtracted(): " + fileName + " removed."); | |
} else { | |
Log.e(LOG_TAG, "removeExtracted(): " + fileName + " not removed!"); | |
} | |
} | |
/** | |
* Return path to asset file in app data folder. Use null for folder path. | |
* | |
* @param fileName asset file name. | |
* @return full path in app directory. | |
*/ | |
private String getAssetsPath(final String fileName) { | |
return getAppDirectory() + "/assets" + ((fileName != null) ? ("/" + fileName) : ""); | |
} | |
/////////////// | |
// Exceptions | |
/////////////// | |
/** | |
* Must be thrown when database file is corrupted and can not be used. | |
*/ | |
public class DatabaseCorruptionException extends Exception { | |
public DatabaseCorruptionException(final String name) { | |
super(String.format("Database %s is corrupted and can't be open.", name)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment