Database: Split read and write locks

This commit is contained in:
José Rebelo
2026-02-10 20:55:40 +00:00
parent 1a8736621f
commit 3e0084e188
15 changed files with 391 additions and 363 deletions
@@ -41,7 +41,6 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
@@ -70,18 +69,13 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import ch.qos.logback.core.spi.LifeCycle;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothStateChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -111,17 +105,14 @@ import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
public class GBApplication extends Application {
// Since this class must not log to slf4j, we use plain android.util.Log
private static final String TAG = "GBApplication";
public static final String DATABASE_NAME = "Gadgetbridge";
private static volatile ShutdownHook SHUTDOWN_HOOK;
private static GBApplication context;
private static final Lock dbLock = new ReentrantLock();
private static DeviceService deviceService;
private static SharedPreferences sharedPrefs;
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
private static GBPrefs prefs;
private static LockHandler lockHandler;
/**
* Note: is null on Lollipop
*/
@@ -178,6 +169,7 @@ public class GBApplication extends Application {
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
GBDatabaseManager.closeDatabase();
System.exit(0);
}
@@ -188,13 +180,7 @@ public class GBApplication extends Application {
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
if (lockHandler != null) {
try {
lockHandler.closeDb();
} catch (final Exception e) {
GB.log("Failed to close DB before restart", GB.ERROR, e);
}
}
GBDatabaseManager.closeDatabase();
final Intent startActivity = new Intent(context, ControlCenterv2.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(
@@ -268,15 +254,16 @@ public class GBApplication extends Application {
@Override
public void onCreate() {
app = this;
super.onCreate();
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
if (lockHandler != null) {
if (app != null) {
super.onCreate();
// guard against multiple invocations (robolectric)
return;
}
app = this;
super.onCreate();
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs = new GBPrefs(sharedPrefs);
@@ -284,7 +271,7 @@ public class GBApplication extends Application {
GBEnvironment.setupEnvironment(GBEnvironment.createDeviceEnvironment());
// setup db after the environment is set up, but don't do it in test mode
// in test mode, it's done individually, see TestBase
setupDatabase();
GBDatabaseManager.setupDatabase(this);
}
// don't do anything here before we set up logging, otherwise
@@ -430,22 +417,6 @@ public class GBApplication extends Application {
return prefs.getBoolean("minimize_priority", false);
}
public void setupDatabase() {
DaoMaster.OpenHelper helper;
GBEnvironment env = GBEnvironment.env();
if (env.isTest()) {
helper = new DaoMaster.DevOpenHelper(this, null, null);
} else {
helper = new DBOpenHelper(this, DATABASE_NAME, null);
}
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
if (lockHandler == null) {
lockHandler = new LockHandler();
}
lockHandler.init(daoMaster, helper);
}
public static Context getContext() {
return context;
}
@@ -471,37 +442,17 @@ public class GBApplication extends Application {
}
/**
* Returns the DBHandler instance for reading/writing or throws GBException
* when that was not successful
* If acquiring was successful, callers must call #releaseDB when they
* are done (from the same thread that acquired the lock!
* <p>
* Callers must not hold a reference to the returned instance because it
* will be invalidated at some point.
*
* @return the DBHandler
* @throws GBException
* @see #releaseDB()
* @see GBDatabaseManager#acquireWrite()
*/
public static DBHandler acquireDB() throws GBException {
try {
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
return lockHandler;
}
} catch (InterruptedException ex) {
Log.i(TAG, "Interrupted while waiting for DB lock");
}
throw new GBException("Unable to access the database.");
return GBDatabaseManager.acquireWrite();
}
/**
* Releases the database lock.
*
* @throws IllegalMonitorStateException if the current thread is not owning the lock
* @see #acquireDB()
* @see GBDatabaseManager#acquireReadOnly()
*/
public static void releaseDB() {
dbLock.unlock();
public static DBHandler acquireDbReadOnly() throws GBException {
return GBDatabaseManager.acquireReadOnly();
}
public static boolean isRunningNougatOrLater() {
@@ -708,35 +659,6 @@ public class GBApplication extends Application {
return packageName;
}
/**
* Deletes both the old Activity database and the new one recreates it with empty tables.
*
* @return true on successful deletion
*/
public static synchronized boolean deleteActivityDatabase(Context context) {
// TODO: flush, close, reopen db
if (lockHandler != null) {
lockHandler.closeDb();
}
boolean result = deleteOldActivityDatabase(context);
result &= getContext().deleteDatabase(DATABASE_NAME);
return result;
}
/**
* Deletes the legacy (pre 0.12) Activity database
*
* @return true on successful deletion
*/
public static synchronized boolean deleteOldActivityDatabase(Context context) {
DBHelper dbHelper = new DBHelper(context);
boolean result = true;
if (dbHelper.existsDB("ActivityDatabase")) {
result = getContext().deleteDatabase("ActivityDatabase");
}
return result;
}
@VisibleForTesting
protected void migratePrefsIfNeeded() {
GBPrefsMigrator.migratePrefsIfNeeded(sharedPrefs);
@@ -0,0 +1,125 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
/**
* This class is NOT thread-safe, all calls should be guarded by the upstream write lock.
*/
public class GBDatabase {
private static final String TAG = "GBDatabase";
public static final String DATABASE_NAME = "Gadgetbridge";
private DaoMaster daoMaster = null;
private SQLiteOpenHelper helper = null;
private DaoSession session = null;
public DaoMaster getDaoMaster() {
return daoMaster;
}
public SQLiteOpenHelper getHelper() {
return helper;
}
public DaoSession getSession() {
return session;
}
void closeDatabase() {
if (session == null) {
Log.w(TAG, "Database was already closed");
return;
}
Log.d(TAG, "Trying to close database from " + Thread.currentThread().getName());
session.clear();
session.getDatabase().close();
session = null;
daoMaster = null;
helper = null;
Log.d(TAG, "Database closed");
}
void setupDatabase(final Context context) {
if (session != null) {
Log.w(TAG, "Database already setup");
return;
}
Log.i(TAG, "Setting up database from " + Thread.currentThread().getName());
if (GBEnvironment.env().isTest()) {
helper = new DaoMaster.DevOpenHelper(context, null, null);
} else {
helper = new DBOpenHelper(context, DATABASE_NAME, null);
}
final SQLiteDatabase db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
session = daoMaster.newSession();
if (session == null) {
throw new RuntimeException("Unable to create database session");
}
Log.d(TAG, "Database setup finished");
}
void importDB(final InputStream inputStream) throws IllegalStateException, IOException {
final String dbPath = getClosedDBPath();
try {
final File toFile = new File(dbPath);
FileUtils.copyStreamToFile(inputStream, toFile);
} finally {
setupDatabase(GBApplication.app());
}
// Validate database - must be AFTER setupDatabase so that helper is not null
if (!helper.getReadableDatabase().isDatabaseIntegrityOk()) {
throw new IOException("Database integrity is not OK");
}
}
void exportDB(final File destFile) throws IllegalStateException, IOException {
try {
final String dbPath = getClosedDBPath();
final File sourceFile = new File(dbPath);
FileUtils.copyFile(sourceFile, destFile);
} finally {
setupDatabase(GBApplication.app());
}
}
void exportDB(final OutputStream dest) throws IOException {
try {
final String dbPath = getClosedDBPath();
final File source = new File(dbPath);
FileUtils.copyFileToStream(source, dest);
} finally {
setupDatabase(GBApplication.app());
}
}
/**
* Closes the database and returns its name.
* Important: after calling this, you have set the database up again.
*/
String getClosedDBPath() throws IllegalStateException {
final SQLiteDatabase db = daoMaster.getDatabase();
final String path = db.getPath();
closeDatabase();
if (db.isOpen()) { // reference counted, so may still be open
throw new IllegalStateException("Database must be closed");
}
return path;
}
}
@@ -0,0 +1,155 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
public class GBDatabaseManager {
private static final String TAG = "GBDatabaseManager";
private static final ReentrantReadWriteLock DB_LOCK = new ReentrantReadWriteLock(true);
private static final GBDatabase GB_DATABASE = new GBDatabase();
private GBDatabaseManager() {
}
public static void closeDatabase() {
Log.v(TAG, "Trying to close database from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.closeDatabase();
} finally {
DB_LOCK.writeLock().unlock();
}
}
public static void setupDatabase(final Context context) {
Log.v(TAG, "Setting up database from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.setupDatabase(context);
} finally {
DB_LOCK.writeLock().unlock();
}
}
/**
* Returns the DBHandler instance for reading/writing or throws GBException
* when that was not successful
* If acquiring was successful, callers must call close when they
* are done (from the same thread that acquired the lock!
* <p>
* Callers must not hold a reference to the returned instance because it
* will be invalidated at some point.
*
* @return the DBHandler
*/
public static DBHandler acquireWrite() throws GBException {
try {
Log.v(TAG, "Trying to acquire write lock from " + Thread.currentThread().getName());
if (DB_LOCK.writeLock().tryLock(30, TimeUnit.SECONDS)) {
Log.v(TAG, "Acquired write lock from " + Thread.currentThread().getName());
return new LockHandler(DB_LOCK.writeLock(), GB_DATABASE.getDaoMaster(), GB_DATABASE.getSession());
}
} catch (final InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for DB lock");
}
throw new GBException("Failed to acquire database write lock");
}
public static DBHandler acquireReadOnly() throws GBException {
try {
Log.v(TAG, "Trying to acquire read lock from " + Thread.currentThread().getName());
if (DB_LOCK.readLock().tryLock(30, TimeUnit.SECONDS)) {
Log.v(TAG, "Acquired read lock from " + Thread.currentThread().getName());
return new LockHandler(DB_LOCK.readLock(), GB_DATABASE.getDaoMaster(), GB_DATABASE.getSession());
}
} catch (final InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for DB lock");
}
throw new GBException("Failed to acquire database read lock");
}
/**
* Deletes both the old Activity database and the new one recreates it with empty tables.
*
* @return true on successful deletion
*/
public static boolean deleteActivityDatabase(final Context context) {
Log.v(TAG, "Deleting activity database from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.closeDatabase();
boolean result = deleteOldActivityDatabase(context);
result &= context.deleteDatabase(GBDatabase.DATABASE_NAME);
return result;
} finally {
GB_DATABASE.setupDatabase(context);
DB_LOCK.writeLock().unlock();
}
}
public static void exportDB(final File destFile) throws IOException {
Log.v(TAG, "Exporting database to file from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.exportDB(destFile);
} finally {
DB_LOCK.writeLock().unlock();
}
}
public static void exportDB(final OutputStream dest) throws IOException {
Log.v(TAG, "Exporting database to OutputStream from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.exportDB(dest);
} finally {
DB_LOCK.writeLock().unlock();
}
}
/**
* Deletes the legacy (pre 0.12) Activity database
*
* @return true on successful deletion
*/
public static boolean deleteOldActivityDatabase(final Context context) {
Log.v(TAG, "Deleting old activity database from " + Thread.currentThread().getName());
final DBHelper dbHelper = new DBHelper(context);
boolean result = true;
if (dbHelper.existsDB("ActivityDatabase")) {
result = context.deleteDatabase("ActivityDatabase");
}
return result;
}
public static void importDB(final File fromFile) throws IllegalStateException, IOException {
importDB(new FileInputStream(fromFile));
}
public static void importDB(final InputStream inputStream) throws IllegalStateException, IOException {
Log.v(TAG, "Importing database from " + Thread.currentThread().getName());
DB_LOCK.writeLock().lock();
try {
GB_DATABASE.importDB(inputStream);
} finally {
DB_LOCK.writeLock().unlock();
}
}
}
@@ -17,7 +17,9 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import java.util.concurrent.locks.Lock;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
@@ -27,90 +29,49 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
* Provides low-level access to the database.
*/
public class LockHandler implements DBHandler {
private static final String TAG = "LockHandler";
private DaoMaster daoMaster = null;
private DaoSession session = null;
private SQLiteOpenHelper helper = null;
private final Lock lock;
public LockHandler() {
}
private final DaoMaster daoMaster;
private final DaoSession session;
public void init(DaoMaster daoMaster, DaoMaster.OpenHelper helper) {
if (isValid()) {
throw new IllegalStateException("DB must be closed before initializing it again");
}
if (daoMaster == null) {
throw new IllegalArgumentException("daoMaster must not be null");
}
if (helper == null) {
throw new IllegalArgumentException("helper must not be null");
}
private boolean closed = false;
public LockHandler(final Lock lock,
final DaoMaster daoMaster,
final DaoSession session) {
this.lock = lock;
this.daoMaster = daoMaster;
this.helper = helper;
session = daoMaster.newSession();
if (session == null) {
throw new RuntimeException("Unable to create database session");
}
this.session = session;
}
@Override
public DaoMaster getDaoMaster() {
return daoMaster;
}
private boolean isValid() {
return daoMaster != null;
}
private void ensureValid() {
if (!isValid()) {
private void ensureNotClosed() {
if (closed) {
throw new IllegalStateException("LockHandler is not in a valid state");
}
}
@Override
public void close() {
ensureValid();
GBApplication.releaseDB();
}
@Override
public synchronized void openDb() {
if (session != null) {
throw new IllegalStateException("session must be null");
if (closed) {
Log.w(TAG, lock.getClass().getSimpleName() + " was already closed (" + Thread.currentThread().getName() + ")");
return;
}
// this will create completely new db instances and in turn update this handler through #init()
GBApplication.app().setupDatabase();
}
@Override
public synchronized void closeDb() {
if (session == null) {
throw new IllegalStateException("session must not be null");
}
session.clear();
session.getDatabase().close();
session = null;
helper = null;
daoMaster = null;
}
@Override
public SQLiteOpenHelper getHelper() {
ensureValid();
return helper;
closed = true;
Log.d(TAG, "Releasing " + lock.getClass().getSimpleName() + " from " + Thread.currentThread().getName());
lock.unlock();
}
@Override
public DaoSession getDaoSession() {
ensureValid();
ensureNotClosed();
return session;
}
@Override
public SQLiteDatabase getDatabase() {
ensureValid();
ensureNotClosed();
return daoMaster.getDatabase();
}
}
@@ -19,11 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@@ -45,15 +43,14 @@ import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.files.FileManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences;
@@ -122,19 +119,9 @@ public class DataManagementActivity extends AbstractGBActivity {
dbPath.setText(getExternalPath());
Button exportDBButton = findViewById(R.id.exportDataButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
exportDBButton.setOnClickListener(v -> exportDB());
Button importDBButton = findViewById(R.id.importDataButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importDB();
}
});
importDBButton.setOnClickListener(v -> importDB());
Button showContentDataButton = findViewById(R.id.showContentDataButton);
showContentDataButton.setOnClickListener(v -> {
@@ -223,24 +210,31 @@ public class DataManagementActivity extends AbstractGBActivity {
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.dbmanagementactivity_export_data_title)
.setMessage(R.string.dbmanagementactivity_export_confirmation)
.setPositiveButton(R.string.activity_DB_ExportButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
exportShared();
DBHelper helper = new DBHelper(DataManagementActivity.this);
File dir = FileUtils.getExternalFilesDir();
File destFile = helper.exportDB(dbHandler, dir);
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
.setPositiveButton(R.string.activity_DB_ExportButton, (dialog, which) -> {
exportShared();
try {
final File dir = FileUtils.getExternalFilesDir();
final File destFile = new File(dir, "Gadgetbridge");
if (destFile.exists()) {
final String date = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
File backup = new File(dir, destFile.getName() + "_" + date);
if (!destFile.renameTo(backup)) {
throw new IOException("Unable to rename existing database file");
}
} else if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IOException("Unable to create directory: " + dir.getAbsolutePath());
}
}
GBDatabaseManager.exportDB(destFile);
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
})
.show();
}
@@ -251,27 +245,18 @@ public class DataManagementActivity extends AbstractGBActivity {
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.dbmanagementactivity_import_data_title)
.setMessage(R.string.dbmanagementactivity_overwrite_database_confirmation)
.setPositiveButton(R.string.dbmanagementactivity_overwrite, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DBHelper helper = new DBHelper(DataManagementActivity.this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
helper.importDB(dbHandler, sourceFile);
helper.validateDB(sqLiteOpenHelper);
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
importShared();
.setPositiveButton(R.string.dbmanagementactivity_overwrite, (dialog, which) -> {
try {
final File dir = FileUtils.getExternalFilesDir();
final File sourceFile = new File(dir, "Gadgetbridge");
GBDatabaseManager.importDB(sourceFile);
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
importShared();
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
})
.show();
}
@@ -6,6 +6,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
import nodomain.freeyourgadget.gadgetbridge.util.GB
@@ -83,7 +84,7 @@ class DatabaseDebugFragment : AbstractDebugFragment() {
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
.setPositiveButton(R.string.Delete) { _, _ ->
if (GBApplication.deleteActivityDatabase(requireContext())) {
if (GBDatabaseManager.deleteActivityDatabase(requireContext())) {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_database_successfully_deleted),
@@ -112,7 +113,7 @@ class DatabaseDebugFragment : AbstractDebugFragment() {
.setIcon(R.drawable.ic_warning)
.setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation)
.setPositiveButton(R.string.Delete) { _, _ ->
if (GBApplication.deleteOldActivityDatabase(requireContext())) {
if (GBDatabaseManager.deleteOldActivityDatabase(requireContext())) {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted),
@@ -18,36 +18,16 @@
package nodomain.freeyourgadget.gadgetbridge.database;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
/**
* Provides low-level access to the database.
*/
public interface DBHandler extends AutoCloseable {
/**
* Closes the database.
*/
void closeDb();
/**
* Opens the database. Note that this is only possible after an explicit
* #closeDb(). Initially the db is implicitly open.
*/
void openDb();
SQLiteOpenHelper getHelper();
/**
* Releases the DB handler. No DB access will be possible before
* #openDb() will be called.
*/
void close() throws Exception;
SQLiteDatabase getDatabase();
DaoMaster getDaoMaster();
DaoSession getDaoSession();
}
@@ -21,7 +21,6 @@ package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,18 +29,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.Query;
@@ -77,7 +69,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@@ -98,78 +89,6 @@ public class DBHelper {
this.context = context;
}
/**
* Closes the database and returns its name.
* Important: after calling this, you have to DBHandler#openDb() it again
* to get it back to work.
*/
private String getClosedDBPath(DBHandler dbHandler) throws IllegalStateException {
SQLiteDatabase db = dbHandler.getDatabase();
String path = db.getPath();
dbHandler.closeDb();
if (db.isOpen()) { // reference counted, so may still be open
throw new IllegalStateException("Database must be closed");
}
return path;
}
public File exportDB(DBHandler dbHandler, File toDir) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
try {
File sourceFile = new File(dbPath);
File destFile = new File(toDir, sourceFile.getName());
if (destFile.exists()) {
File backup = new File(toDir, destFile.getName() + "_" + getDate());
destFile.renameTo(backup);
} else if (!toDir.exists()) {
if (!toDir.mkdirs()) {
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
}
}
FileUtils.copyFile(sourceFile, destFile);
return destFile;
} finally {
dbHandler.openDb();
}
}
public void exportDB(DBHandler dbHandler, OutputStream dest) throws IOException {
String dbPath = getClosedDBPath(dbHandler);
try {
File source = new File(dbPath);
FileUtils.copyFileToStream(source, dest);
} finally {
dbHandler.openDb();
}
}
private String getDate() {
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
}
public void importDB(DBHandler dbHandler, File fromFile) throws IllegalStateException, IOException {
importDB(dbHandler, new FileInputStream(fromFile));
}
public void importDB(DBHandler dbHandler, InputStream inputStream) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
try {
File toFile = new File(dbPath);
FileUtils.copyStreamToFile(inputStream, toFile);
} finally {
dbHandler.openDb();
}
}
public void validateDB(SQLiteOpenHelper dbHandler) throws IOException {
try (SQLiteDatabase db = dbHandler.getReadableDatabase()) {
if (!db.isDatabaseIntegrityOk()) {
throw new IOException("Database integrity is not OK");
}
}
}
public static void dropTable(String tableName, SQLiteDatabase db) {
String statement = "DROP TABLE IF EXISTS '" + tableName + "'";
db.execSQL(statement);
@@ -422,7 +341,6 @@ public class DBHelper {
* exists already, it will be updated with the current preferences values. If no device exists
* yet, it will be created in the database.
*
* @param session
* @return the device entity corresponding to the given GBDevice
*/
public static Device getDevice(GBDevice gbDevice, DaoSession session) {
@@ -596,6 +514,7 @@ public class DBHelper {
return createTag(user, name, null, session);
}
@SuppressWarnings("SameParameterValue")
static Tag createTag(@NonNull User user, @NonNull String name, @Nullable String description, @NonNull DaoSession session) {
Tag tag = new Tag();
tag.setUserId(user.getId());
@@ -3,15 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.database
import android.content.Context
import android.content.Intent
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.work.Worker
import androidx.work.WorkerParameters
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import androidx.core.net.toUri
class DatabaseExportWorker(
private val mContext: Context,
@@ -38,11 +39,8 @@ class DatabaseExportWorker(
}
try {
GBApplication.acquireDB().use { dbHandler ->
val helper = DBHelper(mContext)
mContext.contentResolver.openOutputStream(dst.toUri()).use { out ->
helper.exportDB(dbHandler, out)
}
mContext.contentResolver.openOutputStream(dst.toUri()).use { out ->
GBDatabaseManager.exportDB(out)
}
GBApplication.getPrefs().preferences.edit {
@@ -61,15 +61,13 @@ public class SonySWR12HandlerThread extends GBDeviceIoThread {
}
private void processRealTimeHeartRate(EventWithValue event) {
try {
DBHandler dbHandler = GBApplication.acquireDB();
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession());
int timestamp = getTimestamp();
SonySWR12Sample sample = new SonySWR12Sample(timestamp, deviceId, userId, (int) event.value, ActivitySample.NOT_MEASURED, 0, 1);
provider.addGBActivitySample(sample);
GBApplication.releaseDB();
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(GBDevice.EXTRA_DEVICE, getDevice())
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample)
@@ -100,23 +98,20 @@ public class SonySWR12HandlerThread extends GBDeviceIoThread {
}
private void addActivity(ActivityWithData activity) {
try {
DBHandler dbHandler = GBApplication.acquireDB();
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession());
int kind = SonySWR12Constants.TYPE_ACTIVITY;
SonySWR12Sample sample = new SonySWR12Sample(activity.getTimeStampSec(), deviceId, userId, ActivitySample.NOT_MEASURED, activity.data, kind, 1);
provider.addGBActivitySample(sample);
GBApplication.releaseDB();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
private void addSleep(ActivitySleep activity) {
try {
DBHandler dbHandler = GBApplication.acquireDB();
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession());
@@ -144,7 +139,6 @@ public class SonySWR12HandlerThread extends GBDeviceIoThread {
sample = new SonySWR12Sample(activity.getTimeStampSec() + activity.durationMin * 60, deviceId, userId, ActivitySample.NOT_MEASURED, 0, SonySWR12Constants.TYPE_NOT_WORN, 1);
provider.addGBActivitySample(sample);
}
GBApplication.releaseDB();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
@@ -178,8 +178,7 @@ public class AlarmUtils {
public static List<Alarm> mergeOneshotToDeviceAlarms(GBDevice gbDevice, Alarm oneshot, int position) {
List<Alarm> all_alarms = new ArrayList<>();
try {
DBHandler db = GBApplication.acquireDB();
try (DBHandler db = GBApplication.acquireDB()) {
DaoSession daoSession = db.getDaoSession();
Device device = DBHelper.getDevice(gbDevice, daoSession);
User user = DBHelper.getUser(daoSession);
@@ -188,7 +187,6 @@ public class AlarmUtils {
oneshot.setUserId(user.getId());
daoSession.insertOrReplace(oneshot);
all_alarms = DBHelper.getAlarms(gbDevice);
GBApplication.releaseDB();
} catch (Exception e) {
GB.log("error storing one shot quick alarm", GB.ERROR, e);
}
@@ -40,6 +40,7 @@ import java.util.zip.ZipOutputStream;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
@@ -165,12 +166,7 @@ public class ZipBackupExportJob extends AbstractZipBackupJob {
final ZipEntry zipEntry = new ZipEntry(DATABASE_FILENAME);
zipOut.putNextEntry(zipEntry);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DBHelper helper = new DBHelper(context);
helper.exportDB(dbHandler, zipOut);
} catch (final Exception e) {
throw new IOException("Failed to export database", e);
}
GBDatabaseManager.exportDB(zipOut);
zipOut.closeEntry();
}
@@ -18,7 +18,6 @@ package nodomain.freeyourgadget.gadgetbridge.util.backup;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import org.slf4j.Logger;
@@ -39,6 +38,7 @@ import java.util.zip.ZipFile;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
@@ -140,13 +140,8 @@ public class ZipBackupImportJob extends AbstractZipBackupJob {
// Restore database
LOG.debug("Importing database");
updateProgress(75, R.string.backup_restore_importing_database);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DBHelper helper = new DBHelper(getContext());
final SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
try (InputStream databaseInputStream = zipFile.getInputStream(zipFile.getEntry(DATABASE_FILENAME))) {
helper.importDB(dbHandler, databaseInputStream);
helper.validateDB(sqLiteOpenHelper);
}
try (InputStream databaseInputStream = zipFile.getInputStream(zipFile.getEntry(DATABASE_FILENAME))) {
GBDatabaseManager.importDB(databaseInputStream);
}
if (isAborted()) return;
@@ -214,30 +214,29 @@ internal object RecordedWorkoutSyncer {
sliceStartBoundary: Instant,
sliceEndBoundary: Instant
): List<BaseActivitySummary> {
val db: DBHandler = GBApplication.acquireDB()
try {
val device = DBHelper.getDevice(gbDevice, db.daoSession)
if (device == null) {
LOG.warn("Device not found in database for '{}'", gbDevice.aliasOrName)
return emptyList()
GBApplication.acquireDB().use { db ->
val device = DBHelper.getDevice(gbDevice, db.daoSession)
if (device == null) {
LOG.warn("Device not found in database for '{}'", gbDevice.aliasOrName)
return emptyList()
}
val startDate = Date.from(sliceStartBoundary)
val endDate = Date.from(sliceEndBoundary)
return db.daoSession.baseActivitySummaryDao.queryBuilder()
.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(device.id),
BaseActivitySummaryDao.Properties.StartTime.ge(startDate),
BaseActivitySummaryDao.Properties.StartTime.lt(endDate)
)
.build()
.list()
}
val startDate = Date.from(sliceStartBoundary)
val endDate = Date.from(sliceEndBoundary)
return db.daoSession.baseActivitySummaryDao.queryBuilder()
.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(device.id),
BaseActivitySummaryDao.Properties.StartTime.ge(startDate),
BaseActivitySummaryDao.Properties.StartTime.lt(endDate)
)
.build()
.list()
} catch (e: Exception) {
LOG.error("Error querying workouts from database for device '{}'", gbDevice.aliasOrName, e)
return emptyList()
} finally {
GBApplication.releaseDB()
}
}
@@ -14,6 +14,7 @@ import java.io.File;
import ch.qos.logback.classic.util.ContextInitializer;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBDatabaseManager;
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -70,7 +71,7 @@ public abstract class TestBase {
app = (GBApplication) RuntimeEnvironment.application;
assertNotNull(app);
assertNotNull(getContext());
app.setupDatabase();
GBDatabaseManager.setupDatabase(app);
dbHandler = GBApplication.acquireDB();
daoSession = dbHandler.getDaoSession();
assertNotNull(daoSession);
@@ -78,8 +79,7 @@ public abstract class TestBase {
@After
public void tearDown() throws Exception {
dbHandler.closeDb();
GBApplication.releaseDB();
GBDatabaseManager.closeDatabase();
}
protected GBDevice createDummyGDevice(String macAddress) {