You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

312 lines
10 KiB

/**
* 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;
}
}