/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.sqlite.db.framework;
import android.content.Context;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteCompat;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.util.ProcessLock;
import androidx.sqlite.util.SneakyThrow;
import java.io.File;
import java.util.UUID;
class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
private static final String TAG = "SupportSQLite";
private final Context mContext;
private final String mName;
private final Callback mCallback;
private final boolean mUseNoBackupDirectory;
private final boolean mAllowDataLossOnRecovery;
private final Object mLock;
// Delegate is created lazily
private OpenHelper mDelegate;
private boolean mWriteAheadLoggingEnabled;
FrameworkSQLiteOpenHelper(
Context context,
String name,
Callback callback) {
this(context, name, callback, false);
}
FrameworkSQLiteOpenHelper(
Context context,
String name,
Callback callback,
boolean useNoBackupDirectory) {
this(context, name, callback, useNoBackupDirectory, false);
}
FrameworkSQLiteOpenHelper(
Context context,
String name,
Callback callback,
boolean useNoBackupDirectory,
boolean allowDataLossOnRecovery) {
mContext = context;
mName = name;
mCallback = callback;
mUseNoBackupDirectory = useNoBackupDirectory;
mAllowDataLossOnRecovery = allowDataLossOnRecovery;
mLock = new Object();
}
private OpenHelper getDelegate() {
// getDelegate() is lazy because we don't want to File I/O until the call to
// getReadableDatabase() or getWritableDatabase(). This is better because the call to
// a getReadableDatabase() or a getWritableDatabase() happens on a background thread unless
// queries are allowed on the main thread.
// We defer computing the path the database from the constructor to getDelegate()
// because context.getNoBackupFilesDir() does File I/O :(
synchronized (mLock) {
if (mDelegate == null) {
final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& mName != null
&& mUseNoBackupDirectory) {
File file = new File(
SupportSQLiteCompat.Api21Impl.getNoBackupFilesDir(mContext),
mName
);
mDelegate = new OpenHelper(mContext, file.getAbsolutePath(), dbRef, mCallback,
mAllowDataLossOnRecovery);
} else {
mDelegate = new OpenHelper(mContext, mName, dbRef, mCallback,
mAllowDataLossOnRecovery);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(mDelegate,
mWriteAheadLoggingEnabled);
}
}
return mDelegate;
}
}
@Override
public String getDatabaseName() {
return mName;
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
synchronized (mLock) {
if (mDelegate != null) {
SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(mDelegate, enabled);
}
mWriteAheadLoggingEnabled = enabled;
}
}
@Override
public SupportSQLiteDatabase getWritableDatabase() {
return getDelegate().getSupportDatabase(true);
}
@Override
public SupportSQLiteDatabase getReadableDatabase() {
return getDelegate().getSupportDatabase(false);
}
@Override
public void close() {
getDelegate().close();
}
static class OpenHelper extends SQLiteOpenHelper {
/**
* This is used as an Object reference so that we can access the wrapped database inside
* the constructor. SQLiteOpenHelper requires the error handler to be passed in the
* constructor.
*/
final FrameworkSQLiteDatabase[] mDbRef;
final Context mContext;
final Callback mCallback;
final boolean mAllowDataLossOnRecovery;
// see b/78359448
private boolean mMigrated;
// see b/193182592
private final ProcessLock mLock;
private boolean mOpened;
OpenHelper(Context context, String name, final FrameworkSQLiteDatabase[] dbRef,
final Callback callback, boolean allowDataLossOnRecovery) {
super(context, name, null, callback.version,
new DatabaseErrorHandler() {
@Override
public void onCorruption(SQLiteDatabase dbObj) {
callback.onCorruption(getWrappedDb(dbRef, dbObj));
}
});
mContext = context;
mCallback = callback;
mDbRef = dbRef;
mAllowDataLossOnRecovery = allowDataLossOnRecovery;
mLock = new ProcessLock(name == null ? UUID.randomUUID().toString() : name,
context.getCacheDir(), false);
}
SupportSQLiteDatabase getSupportDatabase(boolean writable) {
try {
mLock.lock(!mOpened && getDatabaseName() != null);
mMigrated = false;
final SQLiteDatabase db = innerGetDatabase(writable);
if (mMigrated) {
// there might be a connection w/ stale structure, we should re-open.
close();
return getSupportDatabase(writable);
}
return getWrappedDb(db);
} finally {
mLock.unlock();
}
}
private SQLiteDatabase innerGetDatabase(boolean writable) {
String name = getDatabaseName();
if (name != null) {
File databaseFile = mContext.getDatabasePath(name);
File parentFile = databaseFile.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
if (!parentFile.isDirectory()) {
Log.w(TAG, "Invalid database parent file, not a directory: " + parentFile);
}
}
}
try {
return getWritableOrReadableDatabase(writable);
} catch (Throwable t) {
// No good, just try again...
super.close();
}
try {
// Wait before trying to open the DB, ideally enough to account for some slow I/O.
// Similar to android_database_SQLiteConnection's BUSY_TIMEOUT_MS but not as much.
Thread.sleep(500);
} catch (InterruptedException e) {
// Ignore, and continue
}
final Throwable openRetryError;
try {
return getWritableOrReadableDatabase(writable);
} catch (Throwable t) {
super.close();
openRetryError = t;
}
if (openRetryError instanceof CallbackException) {
// Callback error (onCreate, onUpgrade, onOpen, etc), possibly user error.
final CallbackException callbackException = (CallbackException) openRetryError;
final Throwable cause = callbackException.getCause();
switch (callbackException.getCallbackName()) {
case ON_CONFIGURE:
case ON_CREATE:
case ON_UPGRADE:
case ON_DOWNGRADE:
SneakyThrow.reThrow(cause);
break;
case ON_OPEN:
default:
break;
}
// If callback exception is not an SQLiteException, then more certainly it is not
// recoverable.
if (!(cause instanceof SQLiteException)) {
SneakyThrow.reThrow(cause);
}
} else if (openRetryError instanceof SQLiteException) {
// Ideally we are looking for SQLiteCantOpenDatabaseException and similar, but
// corruption can manifest in others forms.
if (name == null || !mAllowDataLossOnRecovery) {
SneakyThrow.reThrow(openRetryError);
}
} else {
SneakyThrow.reThrow(openRetryError);
}
// Delete the database and try one last time. (mAllowDataLossOnRecovery == true)
mContext.deleteDatabase(name);
try {
return getWritableOrReadableDatabase(writable);
} catch (CallbackException ex) {
// Unwrap our exception to avoid disruption with other try-catch in the call stack.
SneakyThrow.reThrow(ex.getCause());
return null; // Unreachable code, but compiler doesn't know it.
}
}
private SQLiteDatabase getWritableOrReadableDatabase(boolean writable) {
if (writable) {
return super.getWritableDatabase();
} else {
return super.getReadableDatabase();
}
}
FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) {
return getWrappedDb(mDbRef, sqLiteDatabase);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
try {
mCallback.onCreate(getWrappedDb(sqLiteDatabase));
} catch (Throwable t) {
throw new CallbackException(CallbackName.ON_CREATE, t);
}
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
mMigrated = true;
try {
mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);
} catch (Throwable t) {
throw new CallbackException(CallbackName.ON_UPGRADE, t);
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
try {
mCallback.onConfigure(getWrappedDb(db));
} catch (Throwable t) {
throw new CallbackException(CallbackName.ON_CONFIGURE, t);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
mMigrated = true;
try {
mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);
} catch (Throwable t) {
throw new CallbackException(CallbackName.ON_DOWNGRADE, t);
}
}
@Override
public void onOpen(SQLiteDatabase db) {
if (!mMigrated) {
// if we've migrated, we'll re-open the db so we should not call the callback.
try {
mCallback.onOpen(getWrappedDb(db));
} catch (Throwable t) {
throw new CallbackException(CallbackName.ON_OPEN, t);
}
}
mOpened = true;
}
@Override
@SuppressWarnings("UnsynchronizedOverridesSynchronized") // No need sync due to locks.
public void close() {
try {
mLock.lock();
super.close();
mDbRef[0] = null;
mOpened = false;
} finally {
mLock.unlock();
}
}
static FrameworkSQLiteDatabase getWrappedDb(FrameworkSQLiteDatabase[] refHolder,
SQLiteDatabase sqLiteDatabase) {
FrameworkSQLiteDatabase dbRef = refHolder[0];
if (dbRef == null || !dbRef.isDelegate(sqLiteDatabase)) {
refHolder[0] = new FrameworkSQLiteDatabase(sqLiteDatabase);
}
return refHolder[0];
}
private static final class CallbackException extends RuntimeException {
private final CallbackName mCallbackName;
private final Throwable mCause;
CallbackException(CallbackName callbackName, Throwable cause) {
super(cause);
mCallbackName = callbackName;
mCause = cause;
}
public CallbackName getCallbackName() {
return mCallbackName;
}
@NonNull
@Override
@SuppressWarnings("UnsynchronizedOverridesSynchronized") // Not needed, cause is final
public Throwable getCause() {
return mCause;
}
}
enum CallbackName {
ON_CONFIGURE,
ON_CREATE,
ON_UPGRADE,
ON_DOWNGRADE,
ON_OPEN
}
}
}