Skip to content

Instantly share code, notes, and snippets.

@TheLongRunSmoke
Created October 2, 2018 02:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheLongRunSmoke/9d4ca75723aa568831a805d2697ee0cc to your computer and use it in GitHub Desktop.
Save TheLongRunSmoke/9d4ca75723aa568831a805d2697ee0cc to your computer and use it in GitHub Desktop.
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