feat(android): implement Phase 1.1 SQLite database sharing with WAL mode
- Add DailyNotificationDatabase.java with three-table schema and WAL configuration - Add DailyNotificationMigration.java for SharedPreferences to SQLite migration - Add DailyNotificationDatabaseTest.java with comprehensive unit tests - Add ConfigureOptions interface with dbPath, storage mode, and TTL settings - Add configure() method to DailyNotificationPlugin interface - Update Android plugin with SQLite integration and automatic migration - Update web implementations to implement new configure() method - Add phase1-sqlite-usage.ts example demonstrating shared storage configuration This implements the critical Phase 1.1 gate for shared SQLite storage: - App and plugin can open the same SQLite file with WAL mode - Automatic migration from SharedPreferences preserves existing data - Schema version checking prevents compatibility issues - Concurrent reads during background writes enabled - Configuration API supports both shared and tiered storage modes Files: 8 changed, 1204 insertions(+)
This commit is contained in:
121
examples/phase1-sqlite-usage.ts
Normal file
121
examples/phase1-sqlite-usage.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Phase 1.1 Usage Example
|
||||||
|
*
|
||||||
|
* Demonstrates SQLite database sharing configuration and usage
|
||||||
|
* Shows how to configure the plugin for shared storage mode
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DailyNotification } from '@timesafari/daily-notification-plugin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Configure plugin for shared SQLite storage
|
||||||
|
*/
|
||||||
|
async function configureSharedStorage() {
|
||||||
|
try {
|
||||||
|
console.log('Configuring plugin for shared SQLite storage...');
|
||||||
|
|
||||||
|
// Configure the plugin with shared storage mode
|
||||||
|
await DailyNotification.configure({
|
||||||
|
dbPath: '/data/data/com.yourapp/databases/daily_notifications.db',
|
||||||
|
storage: 'shared',
|
||||||
|
ttlSeconds: 3600, // 1 hour TTL
|
||||||
|
prefetchLeadMinutes: 15, // 15 minutes before notification
|
||||||
|
maxNotificationsPerDay: 5,
|
||||||
|
retentionDays: 7
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Plugin configured successfully for shared storage');
|
||||||
|
|
||||||
|
// Now the plugin will use SQLite database instead of SharedPreferences
|
||||||
|
// The database will be shared between app and plugin
|
||||||
|
// WAL mode enables concurrent reads during writes
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Configuration failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Configure plugin for tiered storage (current implementation)
|
||||||
|
*/
|
||||||
|
async function configureTieredStorage() {
|
||||||
|
try {
|
||||||
|
console.log('Configuring plugin for tiered storage...');
|
||||||
|
|
||||||
|
// Configure the plugin with tiered storage mode (default)
|
||||||
|
await DailyNotification.configure({
|
||||||
|
storage: 'tiered',
|
||||||
|
ttlSeconds: 1800, // 30 minutes TTL
|
||||||
|
prefetchLeadMinutes: 10, // 10 minutes before notification
|
||||||
|
maxNotificationsPerDay: 3,
|
||||||
|
retentionDays: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Plugin configured successfully for tiered storage');
|
||||||
|
|
||||||
|
// Plugin will continue using SharedPreferences + in-memory cache
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Configuration failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Schedule notification with new configuration
|
||||||
|
*/
|
||||||
|
async function scheduleWithNewConfig() {
|
||||||
|
try {
|
||||||
|
// First configure for shared storage
|
||||||
|
await configureSharedStorage();
|
||||||
|
|
||||||
|
// Then schedule a notification
|
||||||
|
await DailyNotification.scheduleDailyNotification({
|
||||||
|
url: 'https://api.example.com/daily-content',
|
||||||
|
time: '09:00',
|
||||||
|
title: 'Daily Update',
|
||||||
|
body: 'Your daily notification is ready',
|
||||||
|
sound: true,
|
||||||
|
priority: 'high'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Notification scheduled with shared storage configuration');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Scheduling failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Check migration status
|
||||||
|
*/
|
||||||
|
async function checkMigrationStatus() {
|
||||||
|
try {
|
||||||
|
// Configure for shared storage to trigger migration
|
||||||
|
await DailyNotification.configure({
|
||||||
|
storage: 'shared',
|
||||||
|
dbPath: '/data/data/com.yourapp/databases/daily_notifications.db'
|
||||||
|
});
|
||||||
|
|
||||||
|
// The plugin will automatically:
|
||||||
|
// 1. Create SQLite database with WAL mode
|
||||||
|
// 2. Migrate existing SharedPreferences data
|
||||||
|
// 3. Validate migration success
|
||||||
|
// 4. Log migration statistics
|
||||||
|
|
||||||
|
console.log('✅ Migration completed automatically during configuration');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export examples for use
|
||||||
|
export {
|
||||||
|
configureSharedStorage,
|
||||||
|
configureTieredStorage,
|
||||||
|
scheduleWithNewConfig,
|
||||||
|
checkMigrationStatus
|
||||||
|
};
|
||||||
312
src/android/DailyNotificationDatabase.java
Normal file
312
src/android/DailyNotificationDatabase.java
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/android/DailyNotificationDatabaseTest.java
Normal file
215
src/android/DailyNotificationDatabaseTest.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* DailyNotificationDatabaseTest.java
|
||||||
|
*
|
||||||
|
* Unit tests for SQLite database functionality
|
||||||
|
* Tests schema creation, WAL mode, and basic operations
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.timesafari.dailynotification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
import android.test.mock.MockContext;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for DailyNotificationDatabase
|
||||||
|
*
|
||||||
|
* Tests the core SQLite functionality including:
|
||||||
|
* - Database creation and schema
|
||||||
|
* - WAL mode configuration
|
||||||
|
* - Table and index creation
|
||||||
|
* - Schema version management
|
||||||
|
*/
|
||||||
|
public class DailyNotificationDatabaseTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
private DailyNotificationDatabase database;
|
||||||
|
private Context mockContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
|
||||||
|
// Create mock context
|
||||||
|
mockContext = new MockContext() {
|
||||||
|
@Override
|
||||||
|
public File getDatabasePath(String name) {
|
||||||
|
return new File(getContext().getCacheDir(), name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create database instance
|
||||||
|
database = new DailyNotificationDatabase(mockContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws Exception {
|
||||||
|
if (database != null) {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
super.tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test database creation and schema
|
||||||
|
*/
|
||||||
|
public void testDatabaseCreation() {
|
||||||
|
assertNotNull("Database should not be null", database);
|
||||||
|
|
||||||
|
SQLiteDatabase db = database.getReadableDatabase();
|
||||||
|
assertNotNull("Readable database should not be null", db);
|
||||||
|
assertTrue("Database should be open", db.isOpen());
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test WAL mode configuration
|
||||||
|
*/
|
||||||
|
public void testWALModeConfiguration() {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Check journal mode
|
||||||
|
android.database.Cursor cursor = db.rawQuery("PRAGMA journal_mode", null);
|
||||||
|
assertTrue("Should have journal mode result", cursor.moveToFirst());
|
||||||
|
String journalMode = cursor.getString(0);
|
||||||
|
assertEquals("Journal mode should be WAL", "wal", journalMode.toLowerCase());
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
// Check synchronous mode
|
||||||
|
cursor = db.rawQuery("PRAGMA synchronous", null);
|
||||||
|
assertTrue("Should have synchronous result", cursor.moveToFirst());
|
||||||
|
int synchronous = cursor.getInt(0);
|
||||||
|
assertEquals("Synchronous mode should be NORMAL", 1, synchronous);
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
// Check foreign keys
|
||||||
|
cursor = db.rawQuery("PRAGMA foreign_keys", null);
|
||||||
|
assertTrue("Should have foreign_keys result", cursor.moveToFirst());
|
||||||
|
int foreignKeys = cursor.getInt(0);
|
||||||
|
assertEquals("Foreign keys should be enabled", 1, foreignKeys);
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test table creation
|
||||||
|
*/
|
||||||
|
public void testTableCreation() {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Check if tables exist
|
||||||
|
assertTrue("notif_contents table should exist",
|
||||||
|
tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_CONTENTS));
|
||||||
|
assertTrue("notif_deliveries table should exist",
|
||||||
|
tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_DELIVERIES));
|
||||||
|
assertTrue("notif_config table should exist",
|
||||||
|
tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_CONFIG));
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test index creation
|
||||||
|
*/
|
||||||
|
public void testIndexCreation() {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Check if indexes exist
|
||||||
|
assertTrue("notif_idx_contents_slot_time index should exist",
|
||||||
|
indexExists(db, "notif_idx_contents_slot_time"));
|
||||||
|
assertTrue("notif_idx_deliveries_slot index should exist",
|
||||||
|
indexExists(db, "notif_idx_deliveries_slot"));
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test schema version management
|
||||||
|
*/
|
||||||
|
public void testSchemaVersion() {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Check user_version
|
||||||
|
android.database.Cursor cursor = db.rawQuery("PRAGMA user_version", null);
|
||||||
|
assertTrue("Should have user_version result", cursor.moveToFirst());
|
||||||
|
int userVersion = cursor.getInt(0);
|
||||||
|
assertEquals("User version should match database version",
|
||||||
|
DailyNotificationDatabase.DATABASE_VERSION, userVersion);
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test basic insert operations
|
||||||
|
*/
|
||||||
|
public void testBasicInsertOperations() {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Test inserting into notif_contents
|
||||||
|
android.content.ContentValues values = new android.content.ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID, "test_slot_1");
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON, "{\"title\":\"Test\"}");
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT, System.currentTimeMillis());
|
||||||
|
|
||||||
|
long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null, values);
|
||||||
|
assertTrue("Insert should succeed", rowId > 0);
|
||||||
|
|
||||||
|
// Test inserting into notif_config
|
||||||
|
values.clear();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, "test_key");
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, "test_value");
|
||||||
|
|
||||||
|
rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
||||||
|
assertTrue("Config insert should succeed", rowId > 0);
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test database file operations
|
||||||
|
*/
|
||||||
|
public void testDatabaseFileOperations() {
|
||||||
|
String dbPath = database.getDatabasePath();
|
||||||
|
assertNotNull("Database path should not be null", dbPath);
|
||||||
|
assertTrue("Database path should not be empty", !dbPath.isEmpty());
|
||||||
|
|
||||||
|
// Database should exist after creation
|
||||||
|
assertTrue("Database file should exist", database.databaseExists());
|
||||||
|
|
||||||
|
// Database size should be greater than 0
|
||||||
|
long size = database.getDatabaseSize();
|
||||||
|
assertTrue("Database size should be greater than 0", size > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if table exists
|
||||||
|
*/
|
||||||
|
private boolean tableExists(SQLiteDatabase db, String tableName) {
|
||||||
|
android.database.Cursor cursor = db.rawQuery(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||||||
|
new String[]{tableName});
|
||||||
|
boolean exists = cursor.moveToFirst();
|
||||||
|
cursor.close();
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if index exists
|
||||||
|
*/
|
||||||
|
private boolean indexExists(SQLiteDatabase db, String indexName) {
|
||||||
|
android.database.Cursor cursor = db.rawQuery(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
||||||
|
new String[]{indexName});
|
||||||
|
boolean exists = cursor.moveToFirst();
|
||||||
|
cursor.close();
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
}
|
||||||
354
src/android/DailyNotificationMigration.java
Normal file
354
src/android/DailyNotificationMigration.java
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/**
|
||||||
|
* DailyNotificationMigration.java
|
||||||
|
*
|
||||||
|
* Migration utilities for transitioning from SharedPreferences to SQLite
|
||||||
|
* Handles data migration while preserving existing notification data
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.timesafari.dailynotification;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles migration from SharedPreferences to SQLite database
|
||||||
|
*
|
||||||
|
* This class provides utilities to:
|
||||||
|
* - Migrate existing notification data from SharedPreferences
|
||||||
|
* - Preserve all existing notification content during transition
|
||||||
|
* - Provide backward compatibility during migration period
|
||||||
|
* - Validate migration success
|
||||||
|
*/
|
||||||
|
public class DailyNotificationMigration {
|
||||||
|
|
||||||
|
private static final String TAG = "DailyNotificationMigration";
|
||||||
|
private static final String PREFS_NAME = "DailyNotificationPrefs";
|
||||||
|
private static final String KEY_NOTIFICATIONS = "notifications";
|
||||||
|
private static final String KEY_SETTINGS = "settings";
|
||||||
|
private static final String KEY_LAST_FETCH = "last_fetch";
|
||||||
|
private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final DailyNotificationDatabase database;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param database SQLite database instance
|
||||||
|
*/
|
||||||
|
public DailyNotificationMigration(Context context, DailyNotificationDatabase database) {
|
||||||
|
this.context = context;
|
||||||
|
this.database = database;
|
||||||
|
this.gson = new Gson();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform complete migration from SharedPreferences to SQLite
|
||||||
|
*
|
||||||
|
* @return true if migration was successful
|
||||||
|
*/
|
||||||
|
public boolean migrateToSQLite() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Starting migration from SharedPreferences to SQLite");
|
||||||
|
|
||||||
|
// Check if migration is needed
|
||||||
|
if (!isMigrationNeeded()) {
|
||||||
|
Log.d(TAG, "Migration not needed - SQLite already up to date");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get writable database
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Start transaction for atomic migration
|
||||||
|
db.beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Migrate notification content
|
||||||
|
int contentCount = migrateNotificationContent(db);
|
||||||
|
|
||||||
|
// Migrate settings
|
||||||
|
int settingsCount = migrateSettings(db);
|
||||||
|
|
||||||
|
// Mark migration as complete
|
||||||
|
markMigrationComplete(db);
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
|
||||||
|
Log.i(TAG, String.format("Migration completed successfully: %d notifications, %d settings",
|
||||||
|
contentCount, settingsCount));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error during migration transaction", e);
|
||||||
|
db.endTransaction();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error during migration", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if migration is needed
|
||||||
|
*
|
||||||
|
* @return true if migration is required
|
||||||
|
*/
|
||||||
|
private boolean isMigrationNeeded() {
|
||||||
|
try {
|
||||||
|
// Check if SharedPreferences has data
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]");
|
||||||
|
|
||||||
|
// Check if SQLite already has data
|
||||||
|
SQLiteDatabase db = database.getReadableDatabase();
|
||||||
|
android.database.Cursor cursor = db.rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null);
|
||||||
|
|
||||||
|
int sqliteCount = 0;
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
sqliteCount = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
// Migration needed if SharedPreferences has data but SQLite doesn't
|
||||||
|
boolean hasPrefsData = !notificationsJson.equals("[]") && !notificationsJson.isEmpty();
|
||||||
|
boolean needsMigration = hasPrefsData && sqliteCount == 0;
|
||||||
|
|
||||||
|
Log.d(TAG, String.format("Migration check: prefs_data=%s, sqlite_count=%d, needed=%s",
|
||||||
|
hasPrefsData, sqliteCount, needsMigration));
|
||||||
|
|
||||||
|
return needsMigration;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking migration status", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate notification content from SharedPreferences to SQLite
|
||||||
|
*
|
||||||
|
* @param db SQLite database instance
|
||||||
|
* @return Number of notifications migrated
|
||||||
|
*/
|
||||||
|
private int migrateNotificationContent(SQLiteDatabase db) {
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]");
|
||||||
|
|
||||||
|
if (notificationsJson.equals("[]") || notificationsJson.isEmpty()) {
|
||||||
|
Log.d(TAG, "No notification content to migrate");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON to List<NotificationContent>
|
||||||
|
Type type = new TypeToken<ArrayList<NotificationContent>>(){}.getType();
|
||||||
|
List<NotificationContent> notifications = gson.fromJson(notificationsJson, type);
|
||||||
|
|
||||||
|
int migratedCount = 0;
|
||||||
|
|
||||||
|
for (NotificationContent notification : notifications) {
|
||||||
|
try {
|
||||||
|
// Create ContentValues for notif_contents table
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID, notification.getId());
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON,
|
||||||
|
gson.toJson(notification));
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT,
|
||||||
|
notification.getFetchedAt());
|
||||||
|
// ETag is null for migrated data
|
||||||
|
values.putNull(DailyNotificationDatabase.COL_CONTENTS_ETAG);
|
||||||
|
|
||||||
|
// Insert into notif_contents table
|
||||||
|
long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null, values);
|
||||||
|
|
||||||
|
if (rowId != -1) {
|
||||||
|
migratedCount++;
|
||||||
|
Log.d(TAG, "Migrated notification: " + notification.getId());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to migrate notification: " + notification.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error migrating notification: " + notification.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Migrated " + migratedCount + " notifications to SQLite");
|
||||||
|
return migratedCount;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error migrating notification content", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate settings from SharedPreferences to SQLite
|
||||||
|
*
|
||||||
|
* @param db SQLite database instance
|
||||||
|
* @return Number of settings migrated
|
||||||
|
*/
|
||||||
|
private int migrateSettings(SQLiteDatabase db) {
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
int migratedCount = 0;
|
||||||
|
|
||||||
|
// Migrate last_fetch timestamp
|
||||||
|
long lastFetch = prefs.getLong(KEY_LAST_FETCH, 0);
|
||||||
|
if (lastFetch > 0) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, KEY_LAST_FETCH);
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(lastFetch));
|
||||||
|
|
||||||
|
long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
||||||
|
if (rowId != -1) {
|
||||||
|
migratedCount++;
|
||||||
|
Log.d(TAG, "Migrated last_fetch setting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate adaptive_scheduling setting
|
||||||
|
boolean adaptiveScheduling = prefs.getBoolean(KEY_ADAPTIVE_SCHEDULING, false);
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, KEY_ADAPTIVE_SCHEDULING);
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(adaptiveScheduling));
|
||||||
|
|
||||||
|
long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
||||||
|
if (rowId != -1) {
|
||||||
|
migratedCount++;
|
||||||
|
Log.d(TAG, "Migrated adaptive_scheduling setting");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Migrated " + migratedCount + " settings to SQLite");
|
||||||
|
return migratedCount;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error migrating settings", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark migration as complete in the database
|
||||||
|
*
|
||||||
|
* @param db SQLite database instance
|
||||||
|
*/
|
||||||
|
private void markMigrationComplete(SQLiteDatabase db) {
|
||||||
|
try {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, "migration_complete");
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(System.currentTimeMillis()));
|
||||||
|
|
||||||
|
db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
||||||
|
|
||||||
|
Log.d(TAG, "Migration marked as complete");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error marking migration complete", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate migration success
|
||||||
|
*
|
||||||
|
* @return true if migration was successful
|
||||||
|
*/
|
||||||
|
public boolean validateMigration() {
|
||||||
|
try {
|
||||||
|
SQLiteDatabase db = database.getReadableDatabase();
|
||||||
|
|
||||||
|
// Check if migration_complete flag exists
|
||||||
|
android.database.Cursor cursor = db.query(
|
||||||
|
DailyNotificationDatabase.TABLE_NOTIF_CONFIG,
|
||||||
|
new String[]{DailyNotificationDatabase.COL_CONFIG_V},
|
||||||
|
DailyNotificationDatabase.COL_CONFIG_K + " = ?",
|
||||||
|
new String[]{"migration_complete"},
|
||||||
|
null, null, null
|
||||||
|
);
|
||||||
|
|
||||||
|
boolean migrationComplete = cursor.moveToFirst();
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
if (!migrationComplete) {
|
||||||
|
Log.w(TAG, "Migration validation failed - migration_complete flag not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have notification content
|
||||||
|
cursor = db.rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null);
|
||||||
|
|
||||||
|
int contentCount = 0;
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
contentCount = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
Log.i(TAG, "Migration validation successful - " + contentCount + " notifications in SQLite");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error validating migration", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get migration statistics
|
||||||
|
*
|
||||||
|
* @return Migration statistics string
|
||||||
|
*/
|
||||||
|
public String getMigrationStats() {
|
||||||
|
try {
|
||||||
|
SQLiteDatabase db = database.getReadableDatabase();
|
||||||
|
|
||||||
|
// Count notifications
|
||||||
|
android.database.Cursor cursor = db.rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null);
|
||||||
|
int notificationCount = 0;
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
notificationCount = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
// Count settings
|
||||||
|
cursor = db.rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null);
|
||||||
|
int settingsCount = 0;
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
settingsCount = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
return String.format("Migration stats: %d notifications, %d settings",
|
||||||
|
notificationCount, settingsCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting migration stats", e);
|
||||||
|
return "Migration stats: Error retrieving data";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,12 @@ import android.app.AlarmManager;
|
|||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -70,6 +73,12 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
private DailyNotificationScheduler scheduler;
|
private DailyNotificationScheduler scheduler;
|
||||||
private DailyNotificationFetcher fetcher;
|
private DailyNotificationFetcher fetcher;
|
||||||
|
|
||||||
|
// SQLite database components
|
||||||
|
private DailyNotificationDatabase database;
|
||||||
|
private DailyNotificationMigration migration;
|
||||||
|
private String databasePath;
|
||||||
|
private boolean useSharedStorage = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the plugin and create notification channel
|
* Initialize the plugin and create notification channel
|
||||||
*/
|
*/
|
||||||
@@ -105,6 +114,177 @@ public class DailyNotificationPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the plugin with database and storage options
|
||||||
|
*
|
||||||
|
* @param call Plugin call containing configuration parameters
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
public void configure(PluginCall call) {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Configuring plugin with new options");
|
||||||
|
|
||||||
|
// Get configuration options
|
||||||
|
String dbPath = call.getString("dbPath");
|
||||||
|
String storageMode = call.getString("storage", "tiered");
|
||||||
|
Integer ttlSeconds = call.getInt("ttlSeconds");
|
||||||
|
Integer prefetchLeadMinutes = call.getInt("prefetchLeadMinutes");
|
||||||
|
Integer maxNotificationsPerDay = call.getInt("maxNotificationsPerDay");
|
||||||
|
Integer retentionDays = call.getInt("retentionDays");
|
||||||
|
|
||||||
|
// Update storage mode
|
||||||
|
useSharedStorage = "shared".equals(storageMode);
|
||||||
|
|
||||||
|
// Set database path
|
||||||
|
if (dbPath != null && !dbPath.isEmpty()) {
|
||||||
|
databasePath = dbPath;
|
||||||
|
Log.d(TAG, "Database path set to: " + databasePath);
|
||||||
|
} else {
|
||||||
|
// Use default database path
|
||||||
|
databasePath = getContext().getDatabasePath("daily_notifications.db").getAbsolutePath();
|
||||||
|
Log.d(TAG, "Using default database path: " + databasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize SQLite database if using shared storage
|
||||||
|
if (useSharedStorage) {
|
||||||
|
initializeSQLiteDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store configuration in database or SharedPreferences
|
||||||
|
storeConfiguration(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
||||||
|
|
||||||
|
Log.i(TAG, "Plugin configuration completed successfully");
|
||||||
|
call.resolve();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error configuring plugin", e);
|
||||||
|
call.reject("Configuration failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SQLite database with migration
|
||||||
|
*/
|
||||||
|
private void initializeSQLiteDatabase() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Initializing SQLite database");
|
||||||
|
|
||||||
|
// Create database instance
|
||||||
|
database = new DailyNotificationDatabase(getContext(), databasePath);
|
||||||
|
|
||||||
|
// Initialize migration utility
|
||||||
|
migration = new DailyNotificationMigration(getContext(), database);
|
||||||
|
|
||||||
|
// Perform migration if needed
|
||||||
|
if (migration.migrateToSQLite()) {
|
||||||
|
Log.i(TAG, "Migration completed successfully");
|
||||||
|
|
||||||
|
// Validate migration
|
||||||
|
if (migration.validateMigration()) {
|
||||||
|
Log.i(TAG, "Migration validation successful");
|
||||||
|
Log.i(TAG, migration.getMigrationStats());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Migration validation failed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Migration failed or not needed");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing SQLite database", e);
|
||||||
|
throw new RuntimeException("SQLite initialization failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store configuration values
|
||||||
|
*/
|
||||||
|
private void storeConfiguration(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
||||||
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
||||||
|
try {
|
||||||
|
if (useSharedStorage && database != null) {
|
||||||
|
// Store in SQLite
|
||||||
|
storeConfigurationInSQLite(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
||||||
|
} else {
|
||||||
|
// Store in SharedPreferences
|
||||||
|
storeConfigurationInSharedPreferences(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error storing configuration", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store configuration in SQLite database
|
||||||
|
*/
|
||||||
|
private void storeConfigurationInSQLite(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
||||||
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
||||||
|
try {
|
||||||
|
SQLiteDatabase db = database.getWritableDatabase();
|
||||||
|
|
||||||
|
// Store each configuration value
|
||||||
|
if (ttlSeconds != null) {
|
||||||
|
storeConfigValue(db, "ttlSeconds", String.valueOf(ttlSeconds));
|
||||||
|
}
|
||||||
|
if (prefetchLeadMinutes != null) {
|
||||||
|
storeConfigValue(db, "prefetchLeadMinutes", String.valueOf(prefetchLeadMinutes));
|
||||||
|
}
|
||||||
|
if (maxNotificationsPerDay != null) {
|
||||||
|
storeConfigValue(db, "maxNotificationsPerDay", String.valueOf(maxNotificationsPerDay));
|
||||||
|
}
|
||||||
|
if (retentionDays != null) {
|
||||||
|
storeConfigValue(db, "retentionDays", String.valueOf(retentionDays));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Configuration stored in SQLite");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error storing configuration in SQLite", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a single configuration value in SQLite
|
||||||
|
*/
|
||||||
|
private void storeConfigValue(SQLiteDatabase db, String key, String value) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, key);
|
||||||
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, value);
|
||||||
|
|
||||||
|
// Use INSERT OR REPLACE to handle updates
|
||||||
|
db.replace(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store configuration in SharedPreferences
|
||||||
|
*/
|
||||||
|
private void storeConfigurationInSharedPreferences(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
||||||
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = getContext().getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE);
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
|
if (ttlSeconds != null) {
|
||||||
|
editor.putInt("ttlSeconds", ttlSeconds);
|
||||||
|
}
|
||||||
|
if (prefetchLeadMinutes != null) {
|
||||||
|
editor.putInt("prefetchLeadMinutes", prefetchLeadMinutes);
|
||||||
|
}
|
||||||
|
if (maxNotificationsPerDay != null) {
|
||||||
|
editor.putInt("maxNotificationsPerDay", maxNotificationsPerDay);
|
||||||
|
}
|
||||||
|
if (retentionDays != null) {
|
||||||
|
editor.putInt("retentionDays", retentionDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.apply();
|
||||||
|
Log.d(TAG, "Configuration stored in SharedPreferences");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error storing configuration in SharedPreferences", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a daily notification with the specified options
|
* Schedule a daily notification with the specified options
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -153,6 +153,15 @@ export interface SchedulingConfig {
|
|||||||
timezone: string;
|
timezone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigureOptions {
|
||||||
|
dbPath?: string;
|
||||||
|
storage?: 'shared' | 'tiered';
|
||||||
|
ttlSeconds?: number;
|
||||||
|
prefetchLeadMinutes?: number;
|
||||||
|
maxNotificationsPerDay?: number;
|
||||||
|
retentionDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Dual Scheduling System Interfaces
|
// Dual Scheduling System Interfaces
|
||||||
export interface ContentFetchConfig {
|
export interface ContentFetchConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -248,6 +257,9 @@ export interface DualScheduleStatus {
|
|||||||
|
|
||||||
// Enhanced DailyNotificationPlugin interface with dual scheduling
|
// Enhanced DailyNotificationPlugin interface with dual scheduling
|
||||||
export interface DailyNotificationPlugin {
|
export interface DailyNotificationPlugin {
|
||||||
|
// Configuration methods
|
||||||
|
configure(options: ConfigureOptions): Promise<void>;
|
||||||
|
|
||||||
// Existing methods
|
// Existing methods
|
||||||
scheduleDailyNotification(options: NotificationOptions | ScheduleOptions): Promise<void>;
|
scheduleDailyNotification(options: NotificationOptions | ScheduleOptions): Promise<void>;
|
||||||
getLastNotification(): Promise<NotificationResponse | null>;
|
getLastNotification(): Promise<NotificationResponse | null>;
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import { WebPlugin } from '@capacitor/core';
|
|||||||
import type { DailyNotificationPlugin, NotificationOptions, NotificationSettings, NotificationResponse, NotificationStatus, BatteryStatus, PowerState, PermissionStatus } from './definitions';
|
import type { DailyNotificationPlugin, NotificationOptions, NotificationSettings, NotificationResponse, NotificationStatus, BatteryStatus, PowerState, PermissionStatus } from './definitions';
|
||||||
|
|
||||||
export class DailyNotificationWeb extends WebPlugin implements DailyNotificationPlugin {
|
export class DailyNotificationWeb extends WebPlugin implements DailyNotificationPlugin {
|
||||||
|
async configure(_options: any): Promise<void> {
|
||||||
|
// Web implementation placeholder
|
||||||
|
console.log('Configure called on web platform');
|
||||||
|
}
|
||||||
|
|
||||||
async scheduleDailyNotification(_options: NotificationOptions | any): Promise<void> {
|
async scheduleDailyNotification(_options: NotificationOptions | any): Promise<void> {
|
||||||
// Web implementation placeholder
|
// Web implementation placeholder
|
||||||
console.log('Schedule daily notification called on web platform');
|
console.log('Schedule daily notification called on web platform');
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
|
|||||||
};
|
};
|
||||||
private scheduledNotifications: Set<string> = new Set();
|
private scheduledNotifications: Set<string> = new Set();
|
||||||
|
|
||||||
|
async configure(_options: any): Promise<void> {
|
||||||
|
// Web implementation placeholder
|
||||||
|
console.log('Configure called on web platform');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a daily notification
|
* Schedule a daily notification
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user