Panagiotis Triantafyllou

db fixes

......@@ -258,8 +258,8 @@
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <provider-->
<!-- android:name=".utils.WarplyProvider"-->
<!-- android:authorities="ly.warp.sdk.utils.WarplyProvider" />-->
<provider
android:name=".utils.WarplyProvider"
android:authorities="ly.warp.sdk.utils.WarplyProvider" />
</application>
</manifest>
\ No newline at end of file
......
......@@ -26,6 +26,11 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import ly.warp.sdk.utils.constants.WarpConstants;
......@@ -40,9 +45,13 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
private static final String DB_NAME = "warply.db";
private static final int DB_VERSION = 12;
private static final int DB_VERSION = 13;
private static final String KEY_CIPHER = "tn#mpOl3v3Dy1pr@W";
// Timeout constants
private static final int CONNECTION_TIMEOUT_MS = 5000; // 5 seconds for database connection
private static final int OPERATION_TIMEOUT_MS = 5000; // 5 seconds for database operations
//------------------------------ Fields -----------------------------//
private static String TABLE_REQUESTS = "requests";
private static String TABLE_PUSH_REQUESTS = "push_requests";
......@@ -119,6 +128,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private SQLiteDatabase mDb;
private static WarplyDBHelper mDBHelperInstance;
// Single-threaded executor for database operations
private final ExecutorService dbExecutor = Executors.newSingleThreadExecutor();
// ===========================================================
// Constructors
// ===========================================================
......@@ -145,21 +157,65 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
/**
* If database connection already initialized, return the db. Else create a
* new one
* Gets a writable database connection on a background thread with timeout.
* This prevents ANR issues by ensuring database operations don't block the main thread.
*/
private SQLiteDatabase getDb() {
if (mDb == null)
mDb = getWritableDatabase(KEY_CIPHER);
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getWritableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout getting writable database connection", e);
// Return null or handle timeout as appropriate
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error getting writable database connection", e);
// Handle other exceptions
}
}
return mDb;
}
/**
* Gets a readable database connection on a background thread with timeout.
* This prevents ANR issues by ensuring database operations don't block the main thread.
*/
private SQLiteDatabase getReadableDb() {
if (mDb == null)
mDb = getReadableDatabase(KEY_CIPHER);
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getReadableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout getting readable database connection", e);
// Return null or handle timeout as appropriate
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error getting readable database connection", e);
// Handle other exceptions
}
}
return mDb;
}
/**
* Close database connection - should only be called when app is being destroyed
* or when database won't be used for a long time
*/
private void closeDb() {
if (mDb != null && mDb.isOpen()) {
mDb.close();
......@@ -167,8 +223,53 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
}
/**
* Initialize database connection on a background thread.
* Call this during app startup to prevent ANR issues.
*/
public void initialize() {
if (mDb == null || !mDb.isOpen()) {
try {
// Submit task to executor and get future
Future<SQLiteDatabase> future = dbExecutor.submit(() -> getWritableDatabase(KEY_CIPHER));
// Wait for result with timeout
mDb = future.get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// Set busy timeout on the database connection
if (mDb != null && mDb.isOpen()) {
mDb.rawExecSQL("PRAGMA busy_timeout = " + CONNECTION_TIMEOUT_MS);
}
} catch (TimeoutException e) {
Log.e("WarplyDBHelper", "Timeout initializing database connection", e);
} catch (Exception e) {
Log.e("WarplyDBHelper", "Error initializing database connection", e);
}
}
}
/**
* Shutdown database connection and executor service.
* Call this when app is being destroyed.
*/
public void shutdown() {
closeDb();
// Shutdown executor service
dbExecutor.shutdown();
try {
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
dbExecutor.shutdownNow();
}
} catch (InterruptedException e) {
dbExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
@Override
public void onCreate(SQLiteDatabase db) {
// Create tables
db.execSQL(CREATE_TABLE_REQUESTS);
db.execSQL(CREATE_TABLE_TAGS);
db.execSQL(CREATE_TABLE_PUSH_REQUESTS);
......@@ -176,6 +277,14 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
db.execSQL(CREATE_TABLE_CLIENT);
db.execSQL(CREATE_TABLE_AUTH);
db.execSQL(CREATE_TABLE_TELEMATICS);
// Create indexes for frequently queried columns to improve performance
db.execSQL("CREATE INDEX IF NOT EXISTS idx_requests_force ON " + TABLE_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_requests_date ON " + TABLE_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_requests_force ON " + TABLE_PUSH_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_requests_date ON " + TABLE_PUSH_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_ack_requests_force ON " + TABLE_PUSH_ACK_REQUESTS + "(" + KEY_REQUESTS_FORCE + ")");
db.execSQL("CREATE INDEX IF NOT EXISTS idx_push_ack_requests_date ON " + TABLE_PUSH_ACK_REQUESTS + "(" + KEY_REQUESTS_DATE_ADDED + ")");
}
@Override
......@@ -228,36 +337,36 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized void clearTable(String tableName) {
SQLiteDatabase db = getDb();
db.delete(tableName, null, null);
closeDb();
// Don't close the database here to improve performance
}
public synchronized void insert(String tableName, ContentValues values) {
SQLiteDatabase db = getDb();
db.insert(tableName, null, values);
closeDb();
// Don't close the database here to improve performance
}
public synchronized void update(String tableName, ContentValues values) {
SQLiteDatabase db = getDb();
db.update(tableName, values, null, null);
closeDb();
// Don't close the database here to improve performance
}
public synchronized boolean isTableNotEmpty(String tableName) {
Cursor cursor = getReadableDb().rawQuery("SELECT COUNT(*) FROM " + tableName,
null);
boolean isNotEmpty = false;
Cursor cursor = null;
try {
cursor = getReadableDb().rawQuery("SELECT COUNT(*) FROM " + tableName, null);
if (cursor != null && cursor.moveToFirst()) {
boolean isNotEmpty = cursor.getInt(0) > 0;
isNotEmpty = cursor.getInt(0) > 0;
}
} finally {
if (cursor != null) {
cursor.close();
closeDb();
return isNotEmpty;
} else {
closeDb();
return false;
}
// Don't close the database here to improve performance
}
return isNotEmpty;
}
//------------------------------ Auth -----------------------------//
......@@ -288,30 +397,36 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
@Nullable
public synchronized String getAuthValue(String columnName) {
String columnValue = "";
Cursor cursor = getReadableDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
cursor.moveToFirst();
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return columnValue;
}
@Nullable
public synchronized String getClientValue(String columnName) {
String columnValue = "";
Cursor cursor = getReadableDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
cursor.moveToFirst();
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return columnValue;
}
......@@ -324,21 +439,36 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
//------------------------------ Api requests -----------------------------//
/**
* Gets all requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
KEY_REQUESTS_DATE_ADDED + " asc", "20");
}
/**
* Gets all push requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllPushRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_PUSH_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
KEY_REQUESTS_DATE_ADDED + " asc", "20");
}
/**
* Gets all push acknowledgment requests from the database.
* NOTE: The caller is responsible for closing the returned Cursor when done with it.
*/
public synchronized Cursor getAllPushAckRequests() {
// We don't close the database here because the cursor needs it to remain open
return getReadableDb().query(TABLE_PUSH_ACK_REQUESTS,
new String[]{KEY_REQUESTS_ID,
KEY_REQUESTS_MICROAPP, KEY_REQUESTS_ENTITY}, null, null, null, null,
......@@ -388,18 +518,32 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long getRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDb(), TABLE_REQUESTS);
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_REQUESTS);
// Don't close the database here to improve performance
return count;
}
public synchronized long getPushRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDb(), TABLE_PUSH_REQUESTS);
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_PUSH_REQUESTS);
// Don't close the database here to improve performance
return count;
}
public synchronized long getPushAckRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDb(), TABLE_PUSH_ACK_REQUESTS);
SQLiteDatabase db = getReadableDb();
long count = DatabaseUtils.queryNumEntries(db, TABLE_PUSH_ACK_REQUESTS);
// Don't close the database here to improve performance
return count;
}
public synchronized void deleteRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
......@@ -409,12 +553,20 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_REQUESTS, strFilter.toString(), null);
closeDb();
db.delete(TABLE_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
public synchronized void deletePushRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
......@@ -424,12 +576,20 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_PUSH_REQUESTS, strFilter.toString(), null);
closeDb();
db.delete(TABLE_PUSH_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
public synchronized void deletePushAckRequests(Long... ids) {
if (ids.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < ids.length; i++) {
if (i > 0) {
......@@ -439,85 +599,103 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
strFilter.append("=");
strFilter.append(ids[i]);
}
getDb().delete(TABLE_PUSH_ACK_REQUESTS, strFilter.toString(), null);
closeDb();
db.delete(TABLE_PUSH_ACK_REQUESTS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
public synchronized boolean isForceRequestsExist() {
Cursor cursor = getReadableDb().query(TABLE_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return result;
}
public synchronized boolean isForcePushRequestsExist() {
Cursor cursor = getReadableDb().query(TABLE_PUSH_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_PUSH_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return result;
}
public synchronized boolean isForcePushAckRequestsExist() {
Cursor cursor = getReadableDb().query(TABLE_PUSH_ACK_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
boolean result = false;
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_PUSH_ACK_REQUESTS, null, KEY_REQUESTS_FORCE + "=1",
null, null, null, null);
if (cursor != null) {
result = cursor.getCount() > 0;
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return result;
}
//------------------------------ Tags -----------------------------//
public synchronized void saveTags(String[] tags) {
if (tags != null && tags.length > 0) {
SQLiteDatabase db = getDb();
try {
getDb().beginTransaction();
db.beginTransaction();
ContentValues values = new ContentValues();
for (String tag : tags) {
values.clear();
values.put(KEY_TAG, tag);
values.put(KEY_TAG_LAST_ADD_DATE, System.currentTimeMillis());
insert(TABLE_TAGS, values);
db.insert(TABLE_TAGS, null, values);
}
getDb().setTransactionSuccessful();
db.setTransactionSuccessful();
} catch (SQLException e) {
closeDb();
if (WarpConstants.DEBUG) {
e.printStackTrace();
}
} finally {
getDb().endTransaction();
closeDb();
db.endTransaction();
// Don't close the database here to improve performance
}
}
}
public synchronized void saveTelematics(JSONArray jsonArray) {
if (jsonArray != null && jsonArray.length() > 0) {
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
ContentValues values = new ContentValues();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonobject = jsonArray.optJSONObject(i);
if (jsonobject != null) {
values.clear();
String timestamp = jsonobject.keys().next();
values.put(KEY_TIMESTAMP, timestamp);
JSONObject jobjData = jsonobject.optJSONObject(timestamp);
......@@ -525,13 +703,23 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
values.put(KEY_ACCELERATION, jobjData.optDouble(KEY_ACCELERATION));
values.put(KEY_SPEED, jobjData.optDouble(KEY_SPEED));
}
insert(TABLE_TELEMATICS, values);
db.insert(TABLE_TELEMATICS, null, values);
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
}
public synchronized void removeTags(String[] tags) {
if (tags.length == 0) return;
SQLiteDatabase db = getDb();
try {
db.beginTransaction();
StringBuilder strFilter = new StringBuilder();
for (int i = 0; i < tags.length; i++) {
if (i > 0) {
......@@ -543,8 +731,12 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
strFilter.append(tags[i]);
strFilter.append("'");
}
getDb().delete(TABLE_TAGS, strFilter.toString(), null);
closeDb();
db.delete(TABLE_TAGS, strFilter.toString(), null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// Don't close the database here to improve performance
}
}
public synchronized void removeAllTags() {
......@@ -553,21 +745,22 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
@Nullable
public synchronized String[] getTags() {
List<String> tags = null;
Cursor cursor = getReadableDb().query(TABLE_TAGS, null, null, null, null, null, null);
Cursor cursor = null;
try {
cursor = getReadableDb().query(TABLE_TAGS, null, null, null, null, null, null);
if (cursor != null) {
tags = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
tags.add(cursor.getString(cursor
.getColumnIndex(KEY_TAG)));
tags.add(cursor.getString(cursor.getColumnIndex(KEY_TAG)));
}
}
} finally {
if (cursor != null) {
cursor.close();
}
closeDb();
// Don't close the database here to improve performance
}
return tags != null ? tags.toArray(new String[tags.size()]) : null;
}
......
......@@ -2,6 +2,7 @@ package ly.warp.sdk.utils;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
......@@ -9,6 +10,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import ly.warp.sdk.db.WarplyDBHelper;
/**
* Created by Panagiotis Triantafyllou on 05/Αυγ/2022.
*/
......@@ -16,7 +19,14 @@ public class WarplyProvider extends ContentProvider {
@Override
public boolean onCreate() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
// AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
// Initialize the database helper
Context context = getContext();
if (context != null) {
WarplyDBHelper dbHelper = WarplyDBHelper.getInstance(context);
dbHelper.initialize(); // This will initialize the database connection on a background thread
}
// ViewPump.init(ViewPump.builder()
// .addInterceptor(new CalligraphyInterceptor(
// new CalligraphyConfig.Builder()
......@@ -34,6 +44,18 @@ public class WarplyProvider extends ContentProvider {
return true;
}
@Override
public void shutdown() {
// Get the database helper instance and shut it down
Context context = getContext();
if (context != null) {
WarplyDBHelper dbHelper = WarplyDBHelper.getInstance(context);
dbHelper.shutdown();
}
super.shutdown();
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
......