refactor(storage): migrate fetcher/worker to Room with legacy fallback
- DailyNotificationPlugin: inject Room storage into fetcher - DailyNotificationFetcher: persist to Room first, mirror to legacy - DailyNotificationWorker: read from Room, fallback to legacy; write next schedule to Room Legacy SharedPreferences path deprecated; retained for transitional compatibility. Co-authored-by: Matthew Raymer
This commit is contained in:
@@ -1,312 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationDatabase.java
|
||||
*
|
||||
* SQLite database management for shared notification storage
|
||||
* Implements the three-table schema with WAL mode for concurrent access
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Manages SQLite database for shared notification storage
|
||||
*
|
||||
* This class implements the shared database approach where:
|
||||
* - App owns schema/migrations (PRAGMA user_version)
|
||||
* - Plugin opens the same path with WAL mode
|
||||
* - Background writes are short & serialized
|
||||
* - Foreground reads proceed during background commits
|
||||
*/
|
||||
public class DailyNotificationDatabase extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = "DailyNotificationDatabase";
|
||||
private static final String DATABASE_NAME = "daily_notifications.db";
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
|
||||
// Table names
|
||||
public static final String TABLE_NOTIF_CONTENTS = "notif_contents";
|
||||
public static final String TABLE_NOTIF_DELIVERIES = "notif_deliveries";
|
||||
public static final String TABLE_NOTIF_CONFIG = "notif_config";
|
||||
|
||||
// Column names for notif_contents
|
||||
public static final String COL_CONTENTS_ID = "id";
|
||||
public static final String COL_CONTENTS_SLOT_ID = "slot_id";
|
||||
public static final String COL_CONTENTS_PAYLOAD_JSON = "payload_json";
|
||||
public static final String COL_CONTENTS_FETCHED_AT = "fetched_at";
|
||||
public static final String COL_CONTENTS_ETAG = "etag";
|
||||
|
||||
// Column names for notif_deliveries
|
||||
public static final String COL_DELIVERIES_ID = "id";
|
||||
public static final String COL_DELIVERIES_SLOT_ID = "slot_id";
|
||||
public static final String COL_DELIVERIES_FIRE_AT = "fire_at";
|
||||
public static final String COL_DELIVERIES_DELIVERED_AT = "delivered_at";
|
||||
public static final String COL_DELIVERIES_STATUS = "status";
|
||||
public static final String COL_DELIVERIES_ERROR_CODE = "error_code";
|
||||
public static final String COL_DELIVERIES_ERROR_MESSAGE = "error_message";
|
||||
|
||||
// Column names for notif_config
|
||||
public static final String COL_CONFIG_K = "k";
|
||||
public static final String COL_CONFIG_V = "v";
|
||||
|
||||
// Status values
|
||||
public static final String STATUS_SCHEDULED = "scheduled";
|
||||
public static final String STATUS_SHOWN = "shown";
|
||||
public static final String STATUS_ERROR = "error";
|
||||
public static final String STATUS_CANCELED = "canceled";
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context Application context
|
||||
* @param dbPath Database file path (null for default location)
|
||||
*/
|
||||
public DailyNotificationDatabase(Context context, String dbPath) {
|
||||
super(context, dbPath != null ? dbPath : DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default database location
|
||||
*
|
||||
* @param context Application context
|
||||
*/
|
||||
public DailyNotificationDatabase(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
Log.d(TAG, "Creating database tables");
|
||||
|
||||
// Configure database for WAL mode and concurrent access
|
||||
configureDatabase(db);
|
||||
|
||||
// Create tables
|
||||
createTables(db);
|
||||
|
||||
// Create indexes
|
||||
createIndexes(db);
|
||||
|
||||
Log.i(TAG, "Database created successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
|
||||
|
||||
// For now, drop and recreate tables
|
||||
// In production, implement proper migration logic
|
||||
dropTables(db);
|
||||
onCreate(db);
|
||||
|
||||
Log.i(TAG, "Database upgraded successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(SQLiteDatabase db) {
|
||||
super.onOpen(db);
|
||||
|
||||
// Ensure WAL mode is enabled on every open
|
||||
configureDatabase(db);
|
||||
|
||||
// Verify schema version
|
||||
verifySchemaVersion(db);
|
||||
|
||||
Log.d(TAG, "Database opened with WAL mode");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure database for optimal performance and concurrency
|
||||
*
|
||||
* @param db SQLite database instance
|
||||
*/
|
||||
private void configureDatabase(SQLiteDatabase db) {
|
||||
// Enable WAL mode for concurrent reads during writes
|
||||
db.execSQL("PRAGMA journal_mode=WAL");
|
||||
|
||||
// Set synchronous mode to NORMAL for better performance
|
||||
db.execSQL("PRAGMA synchronous=NORMAL");
|
||||
|
||||
// Set busy timeout to handle concurrent access
|
||||
db.execSQL("PRAGMA busy_timeout=5000");
|
||||
|
||||
// Enable foreign key constraints
|
||||
db.execSQL("PRAGMA foreign_keys=ON");
|
||||
|
||||
// Set cache size for better performance
|
||||
db.execSQL("PRAGMA cache_size=1000");
|
||||
|
||||
Log.d(TAG, "Database configured with WAL mode and optimizations");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all database tables
|
||||
*
|
||||
* @param db SQLite database instance
|
||||
*/
|
||||
private void createTables(SQLiteDatabase db) {
|
||||
// notif_contents: keep history, fast newest-first reads
|
||||
String createContentsTable = String.format(
|
||||
"CREATE TABLE IF NOT EXISTS %s(" +
|
||||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"%s TEXT NOT NULL," +
|
||||
"%s TEXT NOT NULL," +
|
||||
"%s INTEGER NOT NULL," + // epoch ms
|
||||
"%s TEXT," +
|
||||
"UNIQUE(%s, %s)" +
|
||||
")",
|
||||
TABLE_NOTIF_CONTENTS,
|
||||
COL_CONTENTS_ID,
|
||||
COL_CONTENTS_SLOT_ID,
|
||||
COL_CONTENTS_PAYLOAD_JSON,
|
||||
COL_CONTENTS_FETCHED_AT,
|
||||
COL_CONTENTS_ETAG,
|
||||
COL_CONTENTS_SLOT_ID,
|
||||
COL_CONTENTS_FETCHED_AT
|
||||
);
|
||||
|
||||
// notif_deliveries: track many deliveries per slot/time
|
||||
String createDeliveriesTable = String.format(
|
||||
"CREATE TABLE IF NOT EXISTS %s(" +
|
||||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"%s TEXT NOT NULL," +
|
||||
"%s INTEGER NOT NULL," + // epoch ms
|
||||
"%s INTEGER," + // epoch ms
|
||||
"%s TEXT NOT NULL DEFAULT '%s'," +
|
||||
"%s TEXT," +
|
||||
"%s TEXT" +
|
||||
")",
|
||||
TABLE_NOTIF_DELIVERIES,
|
||||
COL_DELIVERIES_ID,
|
||||
COL_DELIVERIES_SLOT_ID,
|
||||
COL_DELIVERIES_FIRE_AT,
|
||||
COL_DELIVERIES_DELIVERED_AT,
|
||||
COL_DELIVERIES_STATUS,
|
||||
STATUS_SCHEDULED,
|
||||
COL_DELIVERIES_ERROR_CODE,
|
||||
COL_DELIVERIES_ERROR_MESSAGE
|
||||
);
|
||||
|
||||
// notif_config: generic configuration KV
|
||||
String createConfigTable = String.format(
|
||||
"CREATE TABLE IF NOT EXISTS %s(" +
|
||||
"%s TEXT PRIMARY KEY," +
|
||||
"%s TEXT NOT NULL" +
|
||||
")",
|
||||
TABLE_NOTIF_CONFIG,
|
||||
COL_CONFIG_K,
|
||||
COL_CONFIG_V
|
||||
);
|
||||
|
||||
db.execSQL(createContentsTable);
|
||||
db.execSQL(createDeliveriesTable);
|
||||
db.execSQL(createConfigTable);
|
||||
|
||||
Log.d(TAG, "Database tables created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database indexes for optimal query performance
|
||||
*
|
||||
* @param db SQLite database instance
|
||||
*/
|
||||
private void createIndexes(SQLiteDatabase db) {
|
||||
// Index for notif_contents: slot_id + fetched_at DESC for newest-first reads
|
||||
String createContentsIndex = String.format(
|
||||
"CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time ON %s(%s, %s DESC)",
|
||||
TABLE_NOTIF_CONTENTS,
|
||||
COL_CONTENTS_SLOT_ID,
|
||||
COL_CONTENTS_FETCHED_AT
|
||||
);
|
||||
|
||||
// Index for notif_deliveries: slot_id for delivery tracking
|
||||
String createDeliveriesIndex = String.format(
|
||||
"CREATE INDEX IF NOT EXISTS notif_idx_deliveries_slot ON %s(%s)",
|
||||
TABLE_NOTIF_DELIVERIES,
|
||||
COL_DELIVERIES_SLOT_ID
|
||||
);
|
||||
|
||||
db.execSQL(createContentsIndex);
|
||||
db.execSQL(createDeliveriesIndex);
|
||||
|
||||
Log.d(TAG, "Database indexes created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop all database tables (for migration)
|
||||
*
|
||||
* @param db SQLite database instance
|
||||
*/
|
||||
private void dropTables(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONTENTS);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_DELIVERIES);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONFIG);
|
||||
|
||||
Log.d(TAG, "Database tables dropped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify schema version compatibility
|
||||
*
|
||||
* @param db SQLite database instance
|
||||
*/
|
||||
private void verifySchemaVersion(SQLiteDatabase db) {
|
||||
try {
|
||||
// Get current user_version
|
||||
android.database.Cursor cursor = db.rawQuery("PRAGMA user_version", null);
|
||||
int currentVersion = 0;
|
||||
if (cursor.moveToFirst()) {
|
||||
currentVersion = cursor.getInt(0);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
Log.d(TAG, "Current schema version: " + currentVersion);
|
||||
|
||||
// Set user_version to match our DATABASE_VERSION
|
||||
db.execSQL("PRAGMA user_version=" + DATABASE_VERSION);
|
||||
|
||||
Log.d(TAG, "Schema version verified and set to " + DATABASE_VERSION);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error verifying schema version", e);
|
||||
throw new RuntimeException("Schema version verification failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database file path
|
||||
*
|
||||
* @return Database file path
|
||||
*/
|
||||
public String getDatabasePath() {
|
||||
return getReadableDatabase().getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database file exists
|
||||
*
|
||||
* @return true if database file exists
|
||||
*/
|
||||
public boolean databaseExists() {
|
||||
File dbFile = new File(getDatabasePath());
|
||||
return dbFile.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database size in bytes
|
||||
*
|
||||
* @return Database file size in bytes
|
||||
*/
|
||||
public long getDatabaseSize() {
|
||||
File dbFile = new File(getDatabasePath());
|
||||
return dbFile.exists() ? dbFile.length() : 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user