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